Home > Mobile >  WPF - Properties of ItemsSource to Dependency Properties
WPF - Properties of ItemsSource to Dependency Properties

Time:11-27

Background

I am making a custom control that has multiple ListBox's. I want to make this control MVVM compliant, so I am keeping any XAML and the code behind agnostic with respect to any ViewModel. One ListBox is simply going to be a list of TextBox's while the other is going to have a canvas as the host to display the data graphically. Both of these ListBox's are children of this custom control. Pseudo example for the custom control template:

<CustomControl>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
    <ListBox1 Grid.Column="0"/>
    <ListBox2 Grid.Column="1"/>
</CustomControl>

The code behind for this custrom control would have a dependency property that will serve as the ItemsSource, fairly standard stuff:

public IEnumerable ItemsSource
{
    get { return (IEnumerable)GetValue(ItemsSourceProperty); }
    set { SetValue(ItemsSourceProperty, value); }
}

public static readonly DependencyProperty ItemsSourceProperty =
    DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(UserControl1), new PropertyMetadata(new PropertyChangedCallback(OnItemsSourcePropertyChanged)));

private static void OnItemsSourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    var control = sender as UserControl1;
    if (control != null)
        control.OnItemsSourceChanged((IEnumerable)e.OldValue, (IEnumerable)e.NewValue);
}

Where I am stuck

Because the two ListBox's are using the same data source but just display the data differently, I want the ItemsSource defined as one of the the parent view's dependency properties to be the ItemsSource for the two children. From the ViewModel side, this items source can be some sort of ObservableCollection<ChildViewModels>, or IEnumerable, or whatever it wants to be.

How can I point to properties from the ItemsSource's ViewModel to dependency properties of the child views?

I was hoping to get something similar to how it could be done outside of a custom view:

Example Parent ViewModel(omitting a lot, assume all functioning):

public class ParentViewModel
{
    public ObservableCollection<ChildViewModel> ChildViewModels;
}

Example ViewModel (omitting INotifyPropertyChanged and associated logic):

public class ChildViewModel
{
    public string Name {get; set;}
    public string ID {get; set;}
    public string Description {get; set;}   
}

Example control (ommitting setting the DataContext, assume set properly):

<ListBox ItemsSource="{Binding ChildViewModels}">
    <ListBox.ItemsTemplate>
        <StackPanel>
            <TextBlock Text="{Binding Name}"/>
            <TextBlock Text ="{Binding Description}"/>
        </StackPanel>
    </ListBox.ItemsTemplate>
</ListBox>

How can I do something similar where I can pass the properties from the ItemsSource to the child views on a custom control?

Many thanks

CodePudding user response:

If I understand correctly what you need, then here is an example.

  1. Add properties for element templates in both lists and style for Canvas.
using System.Collections;
using System.Windows;
using System.Windows.Controls;

namespace Core2022.SO.jgrmn
{
    public class TwoListControl : Control
    {
        static TwoListControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TwoListControl), new FrameworkPropertyMetadata(typeof(TwoListControl)));
        }

        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register(
                nameof(ItemsSource),
                typeof(IEnumerable),
                typeof(TwoListControl),
                new PropertyMetadata((d, e) => ((TwoListControl)d).OnItemsSourceChanged((IEnumerable)e.OldValue, (IEnumerable)e.NewValue)));

        private void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
        {
            //throw new NotImplementedException();
        }

        public DataTemplate TemplateForStack
        {
            get { return (DataTemplate)GetValue(TemplateForStackProperty); }
            set { SetValue(TemplateForStackProperty, value); }
        }

        public static readonly DependencyProperty TemplateForStackProperty =
            DependencyProperty.Register(
                nameof(TemplateForStack),
                typeof(DataTemplate),
                typeof(TwoListControl),
                new PropertyMetadata(null));

        public DataTemplate TemplateForCanvas
        {
            get { return (DataTemplate)GetValue(TemplateForCanvasProperty); }
            set { SetValue(TemplateForCanvasProperty, value); }
        }

        public static readonly DependencyProperty TemplateForCanvasProperty =
            DependencyProperty.Register(
                nameof(TemplateForCanvas),
                typeof(DataTemplate),
                typeof(TwoListControl),
                new PropertyMetadata(null));

        public Style StyleForCanvas
        {
            get { return (Style)GetValue(StyleForCanvasProperty); }
            set { SetValue(StyleForCanvasProperty, value); }
        }

        public static readonly DependencyProperty StyleForCanvasProperty =
            DependencyProperty.Register(
                nameof(StyleForCanvas),
                typeof(Style),
                typeof(TwoListControl),
                new PropertyMetadata(null));
    }
}

In the theme (Themes/Generic.xaml), set bindings to these properties:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:jgrmn="clr-namespace:Core2022.SO.jgrmn">

    <Style TargetType="{x:Type jgrmn:TwoListControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type jgrmn:TwoListControl}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>
                        <ListBox Grid.Column="0"
                                 ItemsSource="{TemplateBinding ItemsSource}"
                                 ItemTemplate="{TemplateBinding TemplateForStack}"/>
                        <ListBox Grid.Column="1"
                                 ItemsSource="{TemplateBinding ItemsSource}"
                                 ItemTemplate="{TemplateBinding TemplateForCanvas}"
                                 ItemContainerStyle="{TemplateBinding StyleForCanvas}">
                            <ItemsControl.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <Canvas/>
                                </ItemsPanelTemplate>
                            </ItemsControl.ItemsPanel>
                        </ListBox>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Window with an example of use:

<Window x:Class="Core2022.SO.jgrmn.TwoListWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Core2022.SO.jgrmn"
        mc:Ignorable="d"
        Title="TwoListWindow" Height="250" Width="400">
    <FrameworkElement.DataContext>
        <CompositeCollection>
            <Point>15 50</Point>
            <Point>50 150</Point>
            <Point>150 50</Point>
            <Point>150 150</Point>
        </CompositeCollection>
    </FrameworkElement.DataContext>
    <Grid>
        <local:TwoListControl ItemsSource="{Binding}">
            <local:TwoListControl.TemplateForStack>
                <DataTemplate>
                    <TextBlock>
                        <TextBlock.Text>
                            <MultiBinding StringFormat="{}Point ({0} {1})">
                                <Binding Path="X"/>
                                <Binding Path="Y"/>
                            </MultiBinding>
                        </TextBlock.Text>
                    </TextBlock>
                </DataTemplate>
            </local:TwoListControl.TemplateForStack>
            <local:TwoListControl.TemplateForCanvas>
                <DataTemplate>
                    <Ellipse Width="10" Height="10" Fill="Red"/>
                </DataTemplate>
            </local:TwoListControl.TemplateForCanvas>
            <local:TwoListControl.StyleForCanvas>
                <Style TargetType="ListBoxItem">
                    <Setter Property="Canvas.Left" Value="{Binding X}"/>
                    <Setter Property="Canvas.Top" Value="{Binding Y}"/>
                </Style>
            </local:TwoListControl.StyleForCanvas>
        </local:TwoListControl>
    </Grid>
</Window>

enter image description here

CodePudding user response:

You must spend all participating controls a ItemsSource property. The idea is to delegate the source collection from the parent to the child controls and finally to the ListBox. The ItemsSource properties should be a dependency property of type IList and not IEnumerable. This way you force the binding source to be of type IList which improves the binding performance.

To allow customization of the actual displayed items, you must either
a) spend every control a ItemTemplate property of type DataTemplate and delegate it to the inner most ListBox.ItemTemplate (similar to the ItemsSource property) or
b) define the template as a resource (implicit template, which is a key less DataTemplate).

The example implements a):

<Window>
  <Window.DataContext>
    <ParentViewModel />
  </Window.DataCOntext>

  <CustomControl ItemsSource="{Binding ChildViewModels}">
    <CustomControl.ItemsTemplate>
      <StackPanel>
        <TextBlock Text="{Binding Name}"/>
        <TextBlock Text ="{Binding Description}"/>
      </StackPanel>
    </CustomControl.ItemsTemplate>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

    <ListBox1 Grid.Column="0"
              ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemsSource}"
              ItemTemplate="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemTemplate}" />
    <ListBox2 Grid.Column="1"
              ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemsSource}"
              ItemTemplate="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemTemplate}" />
  </CustomControl>
</Window>

Inside the child controls (ListBox1 and ListBox2):

<UserControl>
  <ListBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemsSource}"
           ItemTemplate="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemTemplate}" />
</UserControl>
  • Related