Dinamically add controls from recursive ObservableCollection using MVVM
-
Friday, August 03, 2012 12:02 AM
Hi,
I have a recursive ObservableCollection that looks like this:
private ObservableCollection<TypeModel> _types; internal ObservableCollection<TypeModel> Types { get { return _types; } set { _types = value; NotifyPropertyChanged("Types"); } } class TypeModel { public string Name { get; set; } private ObservableCollection<TypeModel> Fields { get; set; } }So, some of the sub items have items on their Fields field, and some of them have just a Name.
My goal is to dynamically populate ComboBoxes with the Names showing new ComboBoxes for the nested sub items.
I'm using the MVVM pattern to accomplish it. Any thoughts?
All Replies
-
Friday, August 03, 2012 1:11 AMModerator
This should get your started ;)
MainWindow.xaml
<Window x:Class="WpfApplication46.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<Style TargetType="{x:Type ComboBox}"> <Setter Property="ItemTemplate" Value="{DynamicResource ComboBoxTemplate}" /> <Setter Property="Margin" Value="10"/> </Style>
<DataTemplate x:Key="ComboBoxTemplate"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" VerticalAlignment="Center"/> <ComboBox ItemsSource="{Binding Fields}" Visibility="{Binding HasChildren, Converter={StaticResource BooleanToVisibilityConverter}}" /> </StackPanel> </DataTemplate> </Window.Resources> <Grid> <ItemsControl ItemsSource="{Binding Types}" ItemTemplate="{DynamicResource ComboBoxTemplate}" VerticalAlignment="Center" HorizontalAlignment="Center"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" IsItemsHost="True"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </Grid> </Window>
MainWindow.xaml.cs
using System.Windows; using System.Collections.ObjectModel; namespace WpfApplication46 { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new MyViewModel(); } public class MyViewModel { public class TypeModel { public string Name { get; set; } public ObservableCollection<TypeModel> Fields { get; set; } public bool HasChildren { get { return (Fields != null && Fields.Count > 0); } } } public ObservableCollection<TypeModel> Types { get; set; } public MyViewModel() { Types = new ObservableCollection<TypeModel>(); for (var a = 0; a < 5; a++) { var lvl1 = new TypeModel { Name = "Type " + a, Fields = new ObservableCollection<TypeModel>() }; for (var b = 0; b < 5; b++) { var lvl2 = new TypeModel { Name = lvl1.Name + "." + b, Fields = new ObservableCollection<TypeModel>() }; for (var c = 0; c < 5; c++) { var lvl3 = new TypeModel { Name = lvl2.Name + "." + c, Fields = new ObservableCollection<TypeModel>() }; for (var d = 0; d < 5; d++) lvl3.Fields.Add(new TypeModel { Name = lvl3.Name + "." + d, Fields = new ObservableCollection<TypeModel>() }); lvl2.Fields.Add(lvl3); } lvl1.Fields.Add(lvl2); } Types.Add(lvl1); } } } } }Best regards,
Pete
#PEJL
- Edited by XAML guyMicrosoft Community Contributor, Moderator Friday, August 03, 2012 1:22 AM
-
Friday, August 03, 2012 2:13 AM
Thanks a lot Pete, nice code. But what should I change to show the new combo boxes under the old ones, instead of nested?
Best Regards,
Guilherme.
-
Friday, August 03, 2012 11:54 AMModerator
Damn, I'd hoped that was good enough for an answer mark! XD
Please provide a diagram, or ideally a hard coded XAML representation of what you want to achieve, as I can't understand how else you would want to use ComboBoxes to show this data.
Regards,
Pete#PEJL
-
Friday, August 03, 2012 3:15 PM
Sorry Pete, your answer was excellent indeed. I'm quite new to XAML so couldn't figure out a way to implement it the way I needed.
The picture shows the specfication that I need to follow, I hope that it clears out.
So, for example, if I select an item on level 2 that doesn't have anything nested, the level 3 wouldn't appear, and the button would be right beside of level 2.
Again, thanks a lot for your time!
-
Friday, August 03, 2012 5:54 PMModerator
A fun friday challange, if I ever heard one!
This is the best I could do in the time I have.
There's a lot of code here, but it's 95% raw dump that Expression Blend gives you when you copy the template of a standard ComboBox.
I have tried to bolden the bits I added.
What I do is restyle the ComboBox ControlTemplate to include an ItemsControl that generates the next level down, if there is one.
I use a CollectionConverter to make the child look like a collection for the ItemsSource of the ItemsControl that makes the child.
Child is generated from an ItemTemplate, which includes the button.
Once you go over all this in detail, you'll see how I did it, and most likely improve it to suit your own design, this was just a rush fun Friday job.
The only problem i had was the Button is on a new line from the ComboBox. That could probably be resolved with further thinking, but I pass this over to you now.
Whether this solution works for you or not, I had fun knocking it up ;)
MainWindow.xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero" x:Class="WpfApplication46.MainWindow" Title="MainWindow" Height="350" Width="525" xmlns:local="clr-namespace:WpfApplication46"> <Window.Resources> <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> <local:VisibilityNegConverter x:Key="VisibilityNegConverter"/> <DataTemplate x:Key="ComboBoxTemplate"> <StackPanel Orientation="Horizontal"> <ComboBox x:Name="Parent" DisplayMemberPath="Name" ItemsSource="{Binding Fields}" Visibility="{Binding HasChildren, Converter={StaticResource BooleanToVisibilityConverter}}" /> <Button Content="Add" Margin="10" Visibility="{Binding HasChildren, Converter={StaticResource VisibilityNegConverter}}" Click="Button_Click" /> </StackPanel> </DataTemplate> <!-- The rest is Expression Blend generated (plus my tweaks) --> <Style x:Key="ComboBoxFocusVisual"> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate> <Rectangle Margin="4,4,21,4" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/> </ControlTemplate> </Setter.Value> </Setter> </Style> <LinearGradientBrush x:Key="ButtonNormalBackground" EndPoint="0,1" StartPoint="0,0"> <GradientStop Color="#F3F3F3" Offset="0"/> <GradientStop Color="#EBEBEB" Offset="0.5"/> <GradientStop Color="#DDDDDD" Offset="0.5"/> <GradientStop Color="#CDCDCD" Offset="1"/> </LinearGradientBrush> <SolidColorBrush x:Key="ButtonNormalBorder" Color="#FF707070"/> <Geometry x:Key="DownArrowGeometry">M 0 0 L 3.5 4 L 7 0 Z</Geometry> <Style x:Key="ComboBoxReadonlyToggleButton" TargetType="{x:Type ToggleButton}"> <Setter Property="OverridesDefaultStyle" Value="true"/> <Setter Property="IsTabStop" Value="false"/> <Setter Property="Focusable" Value="false"/> <Setter Property="ClickMode" Value="Press"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ToggleButton}"> <Microsoft_Windows_Themes:ButtonChrome x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}" SnapsToDevicePixels="true"> <Grid HorizontalAlignment="Right" Width="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}"> <Path x:Name="Arrow" Data="{StaticResource DownArrowGeometry}" Fill="Black" HorizontalAlignment="Center" Margin="3,1,0,0" VerticalAlignment="Center"/> </Grid> </Microsoft_Windows_Themes:ButtonChrome> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="true"> <Setter Property="RenderPressed" TargetName="Chrome" Value="true"/> </Trigger> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Fill" TargetName="Arrow" Value="#AFAFAF"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <LinearGradientBrush x:Key="TextBoxBorder" EndPoint="0,20" MappingMode="Absolute" StartPoint="0,0"> <GradientStop Color="#ABADB3" Offset="0.05"/> <GradientStop Color="#E2E3EA" Offset="0.07"/> <GradientStop Color="#E3E9EF" Offset="1"/> </LinearGradientBrush> <Style x:Key="ComboBoxToggleButton" TargetType="{x:Type ToggleButton}"> <Setter Property="OverridesDefaultStyle" Value="true"/> <Setter Property="IsTabStop" Value="false"/> <Setter Property="Focusable" Value="false"/> <Setter Property="ClickMode" Value="Press"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ToggleButton}"> <Microsoft_Windows_Themes:ButtonChrome x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}" RoundCorners="false" SnapsToDevicePixels="true" Width="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}"> <Path x:Name="Arrow" Data="{StaticResource DownArrowGeometry}" Fill="Black" HorizontalAlignment="Center" Margin="0,1,0,0" VerticalAlignment="Center"/> </Microsoft_Windows_Themes:ButtonChrome> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="true"> <Setter Property="RenderPressed" TargetName="Chrome" Value="true"/> </Trigger> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Fill" TargetName="Arrow" Value="#AFAFAF"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style TargetType="{x:Type ComboBox}" BasedOn="{x:Null}"> <Style.Resources> <local:CollectionConverter x:Key="CollectionConverter"/> </Style.Resources> <Setter Property="FocusVisualStyle" Value="{StaticResource ComboBoxFocusVisual}"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/> <Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/> <Setter Property="BorderBrush" Value="{StaticResource ButtonNormalBorder}"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/> <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/> <Setter Property="Padding" Value="4,3"/> <Setter Property="ScrollViewer.CanContentScroll" Value="true"/> <Setter Property="ScrollViewer.PanningMode" Value="Both"/> <Setter Property="Stylus.IsFlicksEnabled" Value="False"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ComboBox}"> <Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid x:Name="MainGrid" SnapsToDevicePixels="true" HorizontalAlignment="Left"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/> </Grid.ColumnDefinitions> <Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom"> <Microsoft_Windows_Themes:SystemDropShadowChrome x:Name="Shdw" Color="Transparent" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=MainGrid}"> <Border x:Name="DropDownBorder" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"> <ScrollViewer x:Name="DropDownScrollViewer"> <Grid RenderOptions.ClearTypeHint="Enabled"> <Canvas HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0"> <Rectangle x:Name="OpaqueRect" Fill="{Binding Background, ElementName=DropDownBorder}" Height="{Binding ActualHeight, ElementName=DropDownBorder}" Width="{Binding ActualWidth, ElementName=DropDownBorder}"/> </Canvas> <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </Grid> </ScrollViewer> </Border> </Microsoft_Windows_Themes:SystemDropShadowChrome> </Popup> <ToggleButton BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ComboBoxReadonlyToggleButton}"/> <ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Content="{TemplateBinding SelectionBoxItem}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> </Grid> <ItemsControl Margin="20,0,0,0" Grid.Row="1" ItemTemplate="{StaticResource ComboBoxTemplate}" ItemsSource="{Binding SelectedItem, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource CollectionConverter}}" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="HasDropShadow" SourceName="PART_Popup" Value="true"> <Setter Property="Margin" TargetName="Shdw" Value="0,0,5,5"/> <Setter Property="Color" TargetName="Shdw" Value="#71000000"/> </Trigger> <Trigger Property="HasItems" Value="false"> <Setter Property="Height" TargetName="DropDownBorder" Value="95"/> </Trigger> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/> <Setter Property="Background" Value="#FFF4F4F4"/> </Trigger> <Trigger Property="IsGrouping" Value="true"> <Setter Property="ScrollViewer.CanContentScroll" Value="false"/> </Trigger> <Trigger Property="ScrollViewer.CanContentScroll" SourceName="DropDownScrollViewer" Value="false"> <Setter Property="Canvas.Top" TargetName="OpaqueRect" Value="{Binding VerticalOffset, ElementName=DropDownScrollViewer}"/> <Setter Property="Canvas.Left" TargetName="OpaqueRect" Value="{Binding HorizontalOffset, ElementName=DropDownScrollViewer}"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <Grid> <ComboBox ItemsSource="{Binding Types}" VerticalAlignment="Top" HorizontalAlignment="Left" DisplayMemberPath="Name" /> </Grid> </Window>MainWindow.xaml.cs
using System.Windows; using System.Collections.ObjectModel; using System.Windows.Data; using System.Collections.Generic; using System.Windows.Controls; namespace WpfApplication46 { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new MyViewModel(); } public class MyViewModel { public ObservableCollection<TypeModel> Types { get; set; } public MyViewModel() { Types = new ObservableCollection<TypeModel>(); for (var a = 0; a < 5; a++) { var lvl1 = new TypeModel { Name = "Level " + a, Fields = new ObservableCollection<TypeModel>() }; for (var b = 0; b < 5; b++) { var lvl2 = new TypeModel { Name = lvl1.Name + "." + b, Fields = new ObservableCollection<TypeModel>() }; for (var c = 0; c < 5; c++) { var lvl3 = new TypeModel { Name = lvl2.Name + "." + c, Fields = new ObservableCollection<TypeModel>() }; for (var d = 0; d < 5; d++) lvl3.Fields.Add(new TypeModel { Name = lvl3.Name + "." + d, Fields = new ObservableCollection<TypeModel>() }); lvl2.Fields.Add(lvl3); } lvl1.Fields.Add(lvl2); } Types.Add(lvl1); } } } private void Button_Click(object sender, RoutedEventArgs e) { var b = sender as Button; var tm = b.DataContext as TypeModel; MessageBox.Show(tm.Name); } } public class CollectionConverter : IValueConverter { public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value == null) return null; return new List<object> { value }; } public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new System.NotImplementedException(); } } public class VisibilityNegConverter : IValueConverter { public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) { return (bool)value ? Visibility.Collapsed : Visibility.Visible; } public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new System.NotImplementedException(); } } public class TypeModel { public string Name { get; set; } public ObservableCollection<TypeModel> Fields { get; set; } public bool HasChildren { get { return (Fields != null && Fields.Count > 0); } } } }
Regards,
Pete#PEJL
- Marked As Answer by Guilherme Bencke Friday, August 03, 2012 7:46 PM
-
Friday, August 03, 2012 7:47 PMBeautiful solution Pete, thanks a lot! Have a nice weekend :)
-
Friday, August 03, 2012 8:36 PMModerator
Thanks!
As it happens I thought they were fun examples of recursive controls that I enjoyed figuring out, which has given me some new insight into ItemsControls as conditional generators, so I've been posting them on Technet Samples too:
http://code.msdn.microsoft.com/How-to-generate-items-from-51eeeeb3
http://code.msdn.microsoft.com/Recursive-Controls-b565905b
Please rate them if you like them :)
Thanks for the challange!
#PEJL
- Edited by XAML guyMicrosoft Community Contributor, Moderator Saturday, August 04, 2012 4:46 PM

