skip to Main Content

If someone can help me before I go crazy.
I have a User Control who contains a ListBox
I would like to add a property for the SelectedItem to the UserControl, so the parent can get it.
So I used a DependencyProperty

UserControl (VersionList.xaml):

<UserControl
    x:Class="PcVueLauncher.Controls.VersionsList"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:converters="clr-namespace:PcVueLauncher.Converters"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:PcVueLauncher.Controls"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:Background="white"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <FrameworkElement.Resources>
        <ResourceDictionary>
            <converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
        </ResourceDictionary>
    </FrameworkElement.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock
            Grid.Row="0"
            Padding="10"
            Text="Versions" />
        <ListBox
            Grid.Row="1"
            d:ItemsSource="{d:SampleData ItemCount=5}"
            ItemsSource="{Binding Versions}"
            SelectedItem="{Binding SelectedVersion}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="auto" />
                        </Grid.ColumnDefinitions>
                        <TextBlock
                            Grid.Column="0"
                            Margin="5,5,10,5"
                            Text="{Binding VersionName}" />
                        <Button
                            Grid.Column="1"
                            Padding="5"
                            Command="{Binding RemoveVersionCommand}"
                            Content="Remove"
                            Visibility="{Binding CanBeRemoved, Converter={StaticResource BoolToVisibilityConverter}}" />
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>

        </ListBox>
    </Grid>
</UserControl>

UserControl Associated ViewModel (VersionListViewModel)

namespace PcVueLauncher.ViewModels.Controls
{
    public class VersionsListViewModel : ViewModelBase
    {

        private List<VersionPcVue> _versions;
        public List<VersionPcVue> Versions
        {
            get
            {
                return _versions;
            }
            set
            {
                _versions = value;
                OnPropertyChanged(nameof(Versions));
            }
        }

        private VersionPcVue _selectedVersion;
        public VersionPcVue SelectedVersion
        {
            get
            {
                return _selectedVersion;
            }
            set
            {
                _selectedVersion = value;
                OnPropertyChanged(nameof(SelectedVersion));
            }
        }


        public ICommand RemoveVersionCommand { get; }


        public VersionsListViewModel()
        {
            List<VersionPcVue> versionPcVues = new()
            {
                new VersionPcVue{VersionName="V15"},
                new VersionPcVue{VersionName="V12"}
            };
            Versions = versionPcVues;
        }
    }
}

UserControl code behind (VersionList.cs):

public partial class VersionsList : UserControl
{
    public VersionsList()
    {
        InitializeComponent();
    }

    public VersionPcVue SelectedVersion
    {
        get { return (VersionPcVue)GetValue(SelectedVersionProperty); }
        set { SetValue(SelectedVersionProperty, value); }
    }

    //Using a DependencyProperty as the backing store for SelectedVersion.This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedVersionProperty =
        DependencyProperty.Register("SelectedVersion",
            typeof(VersionPcVue),
            typeof(VersionsList),
            new FrameworkPropertyMetadata(
        defaultValue: null,
        flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
        propertyChangedCallback: new PropertyChangedCallback(OnSelectionChanged)));


    private static void OnSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.CoerceValue(SelectedVersionProperty);
    }
}


    // Register a dependency property with the specified property name,
    // property type, owner type, property metadata, and callbacks.
    public static readonly DependencyProperty SelectedVersionProperty = DependencyProperty.Register(
    name: "SelectedVersion",
    propertyType: typeof(VersionPcVue),
    ownerType: typeof(VersionsList),
    typeMetadata: new FrameworkPropertyMetadata(
        defaultValue: null,
        flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
        propertyChangedCallback: new PropertyChangedCallback(OnSelectionChanged)
    ));



    private static void OnSelectionChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        depObj.CoerceValue(SelectedVersionProperty);
    }

In the HomeView which contains the UserControl, I have this :

<UserControl
    x:Class="PcVueLauncher.Views.HomeView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Controls="clr-namespace:PcVueLauncher.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:PcVueLauncher.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:system="clr-namespace:System;assembly=netstandard"
    xmlns:viewmodels="clr-namespace:PcVueLauncher.ViewModels"
    d:Background="White"
    d:DataContext="{d:DesignInstance Type=viewmodels:HomeViewModel}"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <Grid>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="2*" />
        </Grid.ColumnDefinitions>


        <Controls:VersionsList
            x:Name="test"
            Grid.Column="0"
            DataContext="{Binding VersionsListViewModel}"
            SelectedVersion="{Binding DataContext.SelectedVersion, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />


    </Grid>
</UserControl>

And in the associated ViewModel (HomeViewModel)

   public class HomeViewModel : ViewModelBase
    {
        private IProjectService _projectService;

        private VersionPcVue _selectedVersion;
        public VersionPcVue SelectedVersion
        {
            get
            {
                return _selectedVersion;
            }   
            set
            {
                _selectedVersion = value;
                OnPropertyChanged(nameof(SelectedVersion));
            }
        }

        private VersionPcVue _test1;
        public VersionPcVue Test1
        {
            get
            {
                return _test1;
            }
            set
            {
                _test1 = value;
                OnPropertyChanged(nameof(Test1));
            }
        }

        private string _test;
        public string Test
        {
            get
            {
                return _test;
            }
            set
            {
                _test = value;
                OnPropertyChanged(nameof(Test));
            }
        }

        private VersionsListViewModel versionsListViewModel;
        public VersionsListViewModel VersionsListViewModel
        {
            get
            {
                return versionsListViewModel;
            }
            set
            {
                versionsListViewModel = value;
                OnPropertyChanged(nameof(VersionsListViewModel));
            }
        }

        public HomeViewModel(IProjectService projectService)
        {

            _projectService = projectService;

            VersionsListViewModel = new();
        }
    }

When I change the selected item from my user control, nothing happens in the HomeViewModel.
I thought about a binding error, but to try, I changed this

   SelectedVersion="{Binding DataContext.SelectedVersionnnnnnn, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type Grid}}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

And Visual Studio tells me that the SelectedVersionnnnn does not exist in HomeViewModel.

Why can’t I get back the Selected Item back to the SelectedVersion property of my HomeViewModel.

Thanks a lot for your help

2

Answers


  1. In VersionList.xaml :

    <ListBox SelectedItem="{Binding SelectedVersion}" ...
    

    This only bind ListBox.SelectedItem to {DataContext}.SelectedVersion. Then when a item is selected, the dependency property VersionList.SelectedVersion isn’t updated.


    Solution 1 : By view model (without dependency property)

    I think you mixed-up because you try to use a dependency property that is complex. A easy way is to use directly the view model without dependency property.

    In VersionsList.cs, remove SelectedVersionProperty and SelectedVersion members.

    Keep VersionList.xaml with :

    <UserControl x:Class="PcVueLauncher.Controls.VersionsList" />
    ...
        <ListBox
            ...
            ItemsSource="{Binding Versions}"
            SelectedItem="{Binding SelectedVersion}">
    ...
    </UserControl>
    

    So ListBox.SelectedItem is bind to ListBox.DataContext.SelectedVersion. If ListBox.DataContext is VersionsListViewModel, then ListBox.SelectedItem is bind to VersionsListViewModel.SelectedVersion.

    In the parent controls HomeView, it only need to pass a VersionsListViewModel to the VersionList.DataContext :

    <UserControl x:Class="PcVueLauncher.Views.HomeView"
    ...
        <Controls:VersionsList DataContext="{Binding VersionsListViewModel}" />
    ...
    </UserControl>
    

    So HomeView.VersionsList.ListBox.SelectedItem is bind HomeView.DataContext.VersionsListViewModel.SelectedVersion. If HomeView.DataContext is HomeViewModel, then HomeView.VersionsList.ListBox.SelectedItem is bind HomeViewModel.VersionsListViewModel.SelectedVersion.

    Finally, you can remove the member HomeViewModel.SelectedVersion and use HomeViewModel.VersionsListViewModel.SelectedVersion.

    If you want keep the member HomeViewModel.SelectedVersion, then you need to redirect HomeViewModel.SelectedVersion to HomeViewModel.VersionsListViewModel.SelectedVersion in HomeViewModel.cs :

    public class HomeViewModel : ViewModelBase
    {
        private VersionsListViewModel versionsListViewModel;
        public VersionsListViewModel VersionsListViewModel
        {
            get
            {
                return versionsListViewModel;
            }
            set
            {
                if(versionsListViewModel != null)
                    versionsListViewModel.PropertyChanged -= VersionsListViewModel_PropertyChanged;
                versionsListViewModel = value;
                if(versionsListViewModel != null)
                    versionsListViewModel.PropertyChanged += VersionsListViewModel_PropertyChanged;
                OnPropertyChanged(nameof(VersionsListViewModel));
            }
        }
    
        public VersionPcVue SelectedVersion
        {
            get
            {
                return versionsListViewModel.SelectedVersion;
            }   
            set
            {
                versionsListViewModel.SelectedVersion = value;
                OnPropertyChanged(nameof(SelectedVersion));
            }
        }
    
        void VersionsListViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            //Propagate the property changed SelectedVersion
            if(string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == nameof(VersionsListViewModel.SelectedVersion))
                OnPropertyChanged(nameof(SelectedVersion));
        }
    }
    

    The trick is when HomeViewModel.VersionsListViewModel.SelectedVersion is changed, also notify that HomeViewModel.SelectedVersion is changed.


    Solution 2 : By dependency property

    In sumary, you want when a item is selected that set the selected item in VersionsList.SelectedVersion, then you just need to bind ListBox.SelectedItem to VersionsList.SelectedVersion.

    First, add the dependency property SelectedVersion in VersionList.cs :

    public partial class VersionsList : UserControl
    {
        public VersionsList()
        {
            InitializeComponent();
        }
    
        public VersionPcVue SelectedVersion
        {
            get { return (VersionPcVue)GetValue(SelectedVersionProperty); }
            set { SetValue(SelectedVersionProperty, value); }
        }
    
        public static readonly DependencyProperty SelectedVersionProperty =
            DependencyProperty.Register(
                "SelectedVersion",
                typeof(VersionPcVue),
                typeof(VersionsList),
                new FrameworkPropertyMetadata(
                    defaultValue: null,
                    flags: FrameworkPropertyMetadataOptions.AffectsMeasure
                )
            );
    
        public List<VersionPcVue> Versions
        {
            get { return (List<VersionPcVue>)GetValue(VersionsProperty); }
            set { SetValue(VersionsProperty, value); }
        }
     
        public static readonly DependencyProperty VersionsProperty =
            DependencyProperty.Register(
                "Versions",
                typeof(List<VersionPcVue>),
                typeof(VersionsList),
                new FrameworkPropertyMetadata(
                    defaultValue: null,
                    flags: FrameworkPropertyMetadataOptions.AffectsMeasure
                )
            );
    }
    

    In VersionList.xaml :

    <UserControl x:Class="PcVueLauncher.Controls.VersionsList" />
    ...
        <ListBox
            ...
            ItemsSource="{Binding Versions}, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
            SelectedItem="{Binding SelectedVersion, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
    ...
    </UserControl>
    

    {RelativeSource TemplatedParent} indicate the binding refer to the element to which the template, here VersionsList.

    To use the control :

    <UserControl x:Class="PcVueLauncher.Views.HomeView"
    ...
        <Controls:VersionsList
            Versions="{Binding VersionsListViewModel.Versions}" 
            SelectedVersion="{Binding SelectedVersion}"/>
    ...
    </UserControl>
    

    Versions is also changed to harmonize the binding strategy.

    Finally, you can remove the member VersionsListViewModel.SelectedVersion (or use the trick below).


    What to choose?

    With dependency property, the control isn’t link to view model class. I will use this to develop a library to reuse in many application.

    With view model, the control expect specific members in the data context. I will use this in the application solution.

    Login or Signup to reply.
  2. You need to fix several issues:

    1. Don’t explicitly call DependencyObject.CoerceValue from the property changed callback. It is invoked automatically by the dependency property system – before the property changed callback.

    2. Your property does not affect the layout. For the sake of a better performance, don’t set the FrameworkPropertyMetadataOptions.AffectsMeasure flag as it will force a complete layout pass every time the property changes, which is unnecessary in your case. The ListBox.SelectedItem has no influence on the layout of your control. Instead you should consider to configure the property to bind two way by default by setting the FrameworkPropertyMetadataOptions.BindsTwoWayByDefault flag.

    3. a) Bind internals of a Control to the control’s properties and not to the DataContext. Otherwise your control becomes inconvenient to handle (and to write). For example, if you change the DataContext or rename properties on the object returned by the DataContext, you are forced to rewrite the internal bindings to address the new object structure/property names.

      b) This means you have to remove all internal DataContext bindings and introduce a dependency property for each. For example, remove the ListBox.ItemsSource binding to the VersionsListViewModel.Versions property and introduce a e.g., VersionsItemsSource dependency property instead.

    4. For example in HomeView: define all DataContext related Binding relative to the actual DataContext of the UserControl (or FrameworkElement in general) instead of starting a traversal (using Binding.RelativeSource) to find the parent’s DataContext that is the same as the binding target’s DataContext. It’s pointless and only shows that you have not understood how Binding works.

    Fixes

    1 & 2 & 3b

    VersionList.xaml.cs

    public partial class VersionsList : UserControl
    {
        public VersionPcVue SelectedVersionItem
        {
            get => (VersionPcVue)GetValue(SelectedVersionItemProperty); 
            set => SetValue(SelectedVersionItemProperty, value); 
        }
        
        public static readonly DependencyProperty SelectedVersionItemProperty = DependencyProperty.Register(
            "SelectedVersionItem",
            typeof(VersionPcVue),
            typeof(VersionsList),
            new FrameworkPropertyMetadata(default(VersionPcVue), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedVersionChanged));
    
        public IList VersionsItemsSource
        {
            get => (IList)GetValue(VersionsItemsSourceProperty); 
            set => SetValue(VersionsItemsSourceProperty, value); 
        }
        
        public static readonly DependencyProperty VersionsItemsSourceProperty = DependencyProperty.Register(
            "VersionsItemsSource",
            typeof(IList),
            typeof(VersionsList),
            new PropertyMetadata(default));
    
        public VersionsList()
        {
            InitializeComponent();
        }
    
        private static void OnSelectedVersionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {        
        }
    }
    

    3a

    VersionList.xaml
    When authoring a Control, always bind to properties of the control instead to properties of the DataContext:

    <UserControl>
        <FrameworkElement.Resources>
            <ResourceDictionary>
                <converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
            </ResourceDictionary>
        </FrameworkElement.Resources>
    
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBlock
                Grid.Row="0"
                Padding="10"
                Text="Versions" />
            <ListBox Grid.Row="1"
                     d:ItemsSource="{d:SampleData ItemCount=5}"
                     ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=VersionsItemsSource}"
                     SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedVersionItem}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="auto" />
                            </Grid.ColumnDefinitions>
                            <TextBlock
                                Grid.Column="0"
                                Margin="5,5,10,5"
                                Text="{Binding VersionName}" />
                            <Button
                                Grid.Column="1"
                                Padding="5"
                                Command="{Binding RemoveVersionCommand}"
                                Content="Remove"
                                Visibility="{Binding CanBeRemoved, Converter={StaticResource BoolToVisibilityConverter}}" />
                        </Grid>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
    </UserControl>
    

    4

    HomeView.xaml
    Note that the DataContext of the VersionsList control is referencing a VersionsListViewModel instance. You must adjust all bindings accordingly:

    <UserControl>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="2*" />
            </Grid.ColumnDefinitions>
    
            <Controls:VersionsList x:Name="test"
                                   Grid.Column="0"
                                   DataContext="{Binding VersionsListViewModel}"
                                   VersionsItemsSource="{Binding Versions}"
                                   SelectedVersionItem="{Binding SelectedVersion}" />
        </Grid>
    </UserControl>
    

    Remarks

    "Why can’t I get back the Selected Item back to the SelectedVersion
    property of my HomeViewModel."

    Given your current class design and control configuration, your VersionsList control binds to the VersionsListViewModel.SelectedVersion property. It’s not clear what you really want at this point.
    Either delegate the value manually by letting the HomeViewModel listen to VersionsListViewModel.SelectedVersion property changes or drop related properties from the VersionsListViewModel and bind to the HomeViewModel.SelectedVersion directly. A view model class per view/control will result in bad class design/code most of the time. Creating separate classes should be based on different considerations like responsibilities.
    And then you always want to avoid duplicate code (like properties and logic): instead of copying you move code to separate classes.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search