Dinamically add controls from recursive ObservableCollection using MVVM
-
יום שישי 03 אוגוסט 2012 00:02
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?
כל התגובות
-
יום שישי 03 אוגוסט 2012 01:11מנחה דיון
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
- נערך על-ידי XAML guyMicrosoft Community Contributor, Moderator יום שישי 03 אוגוסט 2012 01:22
-
יום שישי 03 אוגוסט 2012 02:13
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.
-
יום שישי 03 אוגוסט 2012 11:54מנחה דיון
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
-
יום שישי 03 אוגוסט 2012 15:15
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!
-
יום שישי 03 אוגוסט 2012 17:54מנחה דיון
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
- סומן כתשובה על-ידי Guilherme Bencke יום שישי 03 אוגוסט 2012 19:46
-
יום שישי 03 אוגוסט 2012 19:47Beautiful solution Pete, thanks a lot! Have a nice weekend :)
-
יום שישי 03 אוגוסט 2012 20:36מנחה דיון
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
- נערך על-ידי XAML guyMicrosoft Community Contributor, Moderator שבת 04 אוגוסט 2012 16:46