Dinamically add controls from recursive ObservableCollection using MVVM

Answered 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


  • יום שישי 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:47
     
     
    Beautiful 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