WPF Re-Usable Datagrid with Pre-Loaded Data inside its VieModel

Mesh Ka 345 Reputation points
2024-01-27T07:54:40.41+00:00

I have a Project in WPF C# .NET 8 that gets data from sql server and display a StudentName, a DataGrid that Contains places where that student is interested in called PlacesOfInterest. In the PlacesOfInterest datagrid, there are three Column ComboBoxes named Country, Province and District respectively. The Country Column is getting a list of all the Countries from a Table Called Countries in the Database which Contains all the Countries in the World. The Province ComboBoxColumn is getting a list of all the Provinces in each Country and the District ComboBoxColumn is getting a list of Districts in each Province. Here is the fully working repos in github.Now I tried to convert the PlacesOfInterest Datagrid to a re-usable control because it is recurring in my project. Then I came up with Something like this:
the Re-usable DataGrid named PlacesDataGrid.xaml:

<UserControl x:Name="userControl" x:Class="DataGridBindingExampleCore2.CustomControls.PlacesDataGrid"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:conveters="clr-namespace:DataGridBindingExampleCore2.Converters"
             xmlns:vm="clr-namespace:DataGridBindingExampleCore2.CustomControls"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <vm:PlacesDataGridViewModel x:Key="viewModel"/>
        <conveters:ConvProvinceID x:Key="ConvProvinceID" />
        <conveters:ConvDistrictID x:Key="ConvDistrictID" />
    </UserControl.Resources>

    <Grid>
        <DataGrid x:Name="DatagridPlaces"
                AutoGenerateColumns="False"
                ItemsSource="{Binding DataGridItemsSource, ElementName=userControl}"
                SelectedItem="{Binding SelectedPlace}"
                ColumnHeaderStyle="{StaticResource MaterialDesignFlatButton}" >
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="Country">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox  DataContext="{Binding ElementName=userControl, Mode=OneWay}"
                                    Width="120"
                                    DisplayMemberPath="{Binding CountryPathvalue}"
                                    ItemsSource="{Binding Countries, Source={StaticResource viewModel}}"
                                    SelectedValue="{Binding CountrySelectedValue}"
                                    SelectedValuePath="{Binding CountryPathvalue}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="Province">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock>
                                <TextBlock.Text>
                                    <MultiBinding Converter="{StaticResource ConvProvinceID}">
                                        <Binding Path="{Binding ProvinceIDvalue}" />
                                        <Binding Path="Provinces" Source="{StaticResource viewModel}" />
                                    </MultiBinding>
                                </TextBlock.Text>
                            </TextBlock>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate DataType="ComboBox">
                            <ComboBox DataContext="{Binding ElementName=userControl, Mode=OneWay}"
                                    Width="120"
                                    DisplayMemberPath="{Binding ProvincePathValue}"
                                    ItemsSource="{Binding CurrentProvinces, Source={StaticResource viewModel}}"
                                    SelectedValue="{Binding ProvinceSelectedValue}"
                                    SelectedValuePath="{Binding ProvincePathValue}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="District">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock>
                                <TextBlock.Text>
                                    <MultiBinding Converter="{StaticResource ConvDistrictID}">
                                        <Binding Path="{Binding DistrictIDvalue}" />
                                        <Binding Path="Districts" Source="{StaticResource viewModel}" />
                                    </MultiBinding>
                                </TextBlock.Text>
                            </TextBlock>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate DataType="ComboBox">
                            <ComboBox  DataContext="{Binding ElementName=userControl, Mode=OneWay}"
                                    Width="120"
                                    DisplayMemberPath="{Binding DistrictPathValue}"
                                    ItemsSource="{Binding CurrentDistricts, Source={StaticResource viewModel}}"
                                    SelectedValue="{Binding DistrictSelectedValue}"
                                    SelectedValuePath="{Binding DistrictPathValue}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</UserControl>


And Here is the Code-Behind of PlacesDatagrid.xaml:

using System.Collections;
using System.Windows;
using System.Windows.Controls;

namespace DataGridBindingExampleCore2.CustomControls
{
    public partial class PlacesDataGrid : UserControl
    {
        public PlacesDataGrid()
        {
            InitializeComponent();
        }



        public static readonly DependencyProperty DataGridItemsSourceProperty =
            DependencyProperty.Register("DataGridItemsSource", typeof(IEnumerable), typeof(PlacesDataGrid), new PropertyMetadata(null));
        public IEnumerable DataGridItemsSource
        {
            get { return (IEnumerable)GetValue(DataGridItemsSourceProperty); }
            set { SetValue(DataGridItemsSourceProperty, value); }
        }


        public static readonly DependencyProperty CountryPathvalueProperty =
            DependencyProperty.Register("CountryPathvalue", typeof(string), typeof(PlacesDataGrid), new PropertyMetadata(null));
        public string CountryPathvalue
        {
            get { return (string)GetValue(CountryPathvalueProperty); }
            set { SetValue(CountryPathvalueProperty, value); }
        }

        public static readonly DependencyProperty CountrySelectedValueProperty =
            DependencyProperty.Register("CountrySelectedValue", typeof(object), typeof(PlacesDataGrid), new PropertyMetadata(null));
        public object CountrySelectedValue
        {
            get { return (object)GetValue(CountrySelectedValueProperty); }
            set { SetValue(CountrySelectedValueProperty, value); }
        }

        public static readonly DependencyProperty ProvincePathValueProperty =
            DependencyProperty.Register("ProvincePathValue", typeof(string), typeof(PlacesDataGrid), new PropertyMetadata(null));
        public string ProvincePathValue
        {
            get { return (string)GetValue(ProvincePathValueProperty); }
            set { SetValue(ProvincePathValueProperty, value); }
        }

        public static readonly DependencyProperty ProvinceSelectedValueProperty =
            DependencyProperty.Register("ProvinceSelectedValue", typeof(object), typeof(PlacesDataGrid), new PropertyMetadata(null));
        public object ProvinceSelectedValue
        {
            get { return (object)GetValue(ProvinceSelectedValueProperty); }
            set { SetValue(ProvinceSelectedValueProperty, value); }
        }

        public static readonly DependencyProperty ProvinceIDvalueProperty =
            DependencyProperty.Register("ProvinceIDvalue", typeof(string), typeof(PlacesDataGrid), new PropertyMetadata(null));
        public string ProvinceIDvalue
        {
            get { return (string)GetValue(ProvinceIDvalueProperty); }
            set { SetValue(ProvinceIDvalueProperty, value); }
        }


        public static readonly DependencyProperty DistrictPathValueProperty =
            DependencyProperty.Register("DistrictPathValue", typeof(string), typeof(PlacesDataGrid), new PropertyMetadata(null));
        public string DistrictPathValue
        {
            get { return (string)GetValue(DistrictPathValueProperty); }
            set { SetValue(DistrictPathValueProperty, value); }
        }

        public static readonly DependencyProperty DistrictSelectedValueProperty =
            DependencyProperty.Register("DistrictSelectedValue", typeof(object), typeof(PlacesDataGrid), new PropertyMetadata(null));
        public object DistrictSelectedValue
        {
            get { return (object)GetValue(DistrictSelectedValueProperty); }
            set { SetValue(DistrictSelectedValueProperty, value); }
        }

        public static readonly DependencyProperty DistrictIDvalueProperty =
            DependencyProperty.Register("DistrictIDvalue", typeof(string), typeof(PlacesDataGrid), new PropertyMetadata(null));
        public string DistrictIDvalue
        {
            get { return (string)GetValue(DistrictIDvalueProperty); }
            set { SetValue(DistrictIDvalueProperty, value); }
        }

    }
}

And Here is the ViewModel named PlacesDataGridViewModel.cs where all the dropdowns of the ComboBoxes are stored and filter accordingly:

using CommunityToolkit.Mvvm.ComponentModel;
using DataGridBindingExampleCore2.Models;
using System.Collections.ObjectModel;
using System.Linq;

namespace DataGridBindingExampleCore2.CustomControls
{
    public partial class PlacesDataGridViewModel : ObservableObject
    {
        public PlacesDataGridViewModel()
        {
            LoadDataAsync();
        }

        private async void LoadDataAsync()
        {
            this.Countries = new ObservableCollection<CountriesModel>(await DAL.LoadCountriesAsync());
            this.Provinces = new ObservableCollection<ProvincesModel>(await DAL.LoadProvincesAsync());
            this.Districts = new ObservableCollection<DistrictsModel>(await DAL.LoadDistrictsAsync());
        }

        [ObservableProperty]
        ObservableCollection<CountriesModel> countries;
        [ObservableProperty]
        ObservableCollection<ProvincesModel> provinces;
        [ObservableProperty]
        ObservableCollection<DistrictsModel> districts;

        public object CurrentProvinces
        { get => new ObservableCollection<ProvincesModel>(Provinces.Where((p) => p.CountryName == SelectedPlace.CountryName)); }
        public object CurrentDistricts
        { get => new ObservableCollection<DistrictsModel>(Districts.Where((p) => p.ProvinceID == SelectedPlace.ProvinceID)); }

        [ObservableProperty]
        PlacesOfInterest selectedPlace;
    }
}

Now the issue is that, How can I tell the CountrySelectedValue, ProvinceSelectedValue, DistrictPathValue that the values “CountryName “ , “ProvinceID” and “DistrictID”  respectively are in the PlacesDataGridVieModel and not in where the Re-Usable Control is used, like here below in MainWindow.

                <customControl:PlacesDataGrid
                    DataGridItemsSource="{Binding SelectedStudent.PlacesOfInterest}"
                    CountryPathvalue="CountryName"
                    CountrySelectedValue="{Binding CountryName, UpdateSourceTrigger=PropertyChanged}"                  
                    ProvincePathValue="ProvinceName"
                    ProvinceSelectedValue="{Binding ProvinceID, UpdateSourceTrigger=PropertyChanged}"
                    ProvinceIDvalue="ProvinceID"
                    DistrictPathValue="DistrictName"
                    DistrictSelectedValue="{Binding DistrictID, UpdateSourceTrigger=PropertyChanged}"
                    DistrictIDvalue="DistrictID"
                />

Here is the repos of the 2nd Version with the Re-Usable UserControl I tried.

And Here is the Database Script of the Database I was Using for Test.

Any help will be highly appreciated!

Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,705 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,561 questions
XAML
XAML
A language based on Extensible Markup Language (XML) that enables developers to specify a hierarchy of objects with a set of properties and logic.
781 questions
0 comments No comments
{count} votes

Accepted answer
  1. Peter Fleischer (former MVP) 19,306 Reputation points
    2024-01-29T16:09:46.8366667+00:00

    Hi

    "1. Why use Code-Behind?" - you start with code-behind in your solution and I'll move on. It's not a problem to separate code into a separate class like ViewModel.

    "... how to re-use ...." with other types - in this case you need to assign the filtering outside of the custom control. There are several ways to solve this problem. I recommend describing the entire solution first and then choosing the best way.

    You can change code in PlacesDataGridViewModel for using with different types of SelectedPlace if you use the same property names in different types:

    		public object CurrentProvinces
    		{
    			get
    			{
    				var countryName = SelectedPlace?.GetType().GetProperty("CountryName")?.GetValue(SelectedPlace, null);
    				if (countryName == null) return null;
    				return new ObservableCollection<ProvincesModel>(Provinces.Where((p) => p.CountryName == countryName.ToString()));
    			}
    		}
    		public object CurrentDistricts
    		{
    			get
    			{
    				var provinceID = SelectedPlace?.GetType().GetProperty("ProvinceID")?.GetValue(SelectedPlace, null);
    				if (provinceID == null) return null;
    				return new ObservableCollection<DistrictsModel>(Districts.Where((p) => p.ProvinceID == (int)provinceID));
    			}
    		}
    		public object SelectedPlace { get; set; }
    

1 additional answer

Sort by: Most helpful
  1. Peter Fleischer (former MVP) 19,306 Reputation points
    2024-01-28T09:47:44.6633333+00:00

    Hi,
    change in MainWindow.xaml:

                    <customControl:PlacesDataGrid 
                        DataGridItemsSource="{Binding SelectedStudent.PlacesOfInterest}"
                        CountryPathValue="CountryName"
                        CountrySelectedValue="CountryName"                  
                        ProvincePathValue="ProvinceName"
                        ProvinceSelectedValue="ProvinceID"
                        ProvinceIDValue="ProvinceID"
                        DistrictPathValue="DistrictName"
                        DistrictSelectedValue="DistrictID"
                        DistrictIDValue="DistrictID"/>
    
    
    

    change your CustomControl - PlacesDataGrid.xaml:

    <UserControl x:Class="DataGridBindingExampleCore2.CustomControls.PlacesDataGrid"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:vm="clr-namespace:DataGridBindingExampleCore2.CustomControls"
                 xmlns:conveters="clr-namespace:DataGridBindingExampleCore2.Converters" 
                 mc:Ignorable="d" 
                 d:DesignHeight="450" d:DesignWidth="800">
      <UserControl.Resources>
        <vm:PlacesDataGridViewModel x:Key="viewModel"/>
      </UserControl.Resources>
    
      <Grid x:Name="grid" DataContext="{StaticResource viewModel}">
        <DataGrid x:Name="dg"
                  AutoGenerateColumns="False"
                  CanUserAddRows="False"
                  ItemsSource="{Binding DataGridItemsSource, RelativeSource={RelativeSource AncestorType=UserControl}}"
                  SelectedItem="{Binding SelectedPlace}">
        </DataGrid>
      </Grid>
    </UserControl>
    

    and codebehind of CustomControl - PlacesDataGrid.xaml.cs:

    	public partial class PlacesDataGrid : UserControl
    	{
    		public PlacesDataGrid()
    		{
    			InitializeComponent();
    			this.Loaded += PlacesDataGrid_Loaded;
    		}
    
    		public static readonly DependencyProperty DataGridItemsSourceProperty =
    				DependencyProperty.Register("DataGridItemsSource", typeof(IEnumerable), typeof(PlacesDataGrid), new PropertyMetadata(null));
    		public IEnumerable DataGridItemsSource
    		{
    			get { return (IEnumerable)GetValue(DataGridItemsSourceProperty); }
    			set { SetValue(DataGridItemsSourceProperty, value); }
    		}
    
    		public string CountryPathValue { get; set; }
    		public string CountrySelectedValue { get; set; }
    		public string ProvincePathValue { get; set; }
    		public string ProvinceSelectedValue { get; set; }
    		public string ProvinceIDValue { get; set; }
    		public string DistrictPathValue { get; set; }
    		public string DistrictSelectedValue { get; set; }
    		public string DistrictIDValue { get; set; }
    		private void PlacesDataGrid_Loaded(object sender, RoutedEventArgs e) => SetColumns();
    
    		private void SetColumns()
    		{
    			// First column CountryPathvalue
    			DataGridTemplateColumn col1 = new() { Header = "Country", Width = 150 };
    			DataTemplate dt1 = new DataTemplate();
    			FrameworkElementFactory cb1 = new FrameworkElementFactory(typeof(ComboBox));
    			cb1.SetValue(ComboBox.DisplayMemberPathProperty, CountryPathValue);
    			cb1.SetBinding(ComboBox.ItemsSourceProperty, new Binding("Countries") { Source = grid.DataContext, Mode = BindingMode.OneWay });
    			cb1.SetBinding(ComboBox.SelectedValueProperty, new Binding(CountrySelectedValue) { Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });
    			cb1.SetValue(ComboBox.SelectedValuePathProperty, CountryPathValue);
    			dt1.VisualTree = cb1;
    			col1.CellTemplate = dt1;
    			dg.Columns.Add(col1);
    
    			// Second column
    			DataGridTemplateColumn col2 = new() { Header = "Province", Width = 150 };
    			DataTemplate dt2a = new DataTemplate();
    			FrameworkElementFactory tb2 = new FrameworkElementFactory(typeof(TextBlock));
    			MultiBinding mb2 = new MultiBinding() { Mode = BindingMode.OneWay };
    			mb2.Converter = new ConvProvinceID();
    			mb2.Bindings.Add(new Binding(ProvinceIDValue));
    			mb2.Bindings.Add(new Binding("Provinces") { Source = grid.DataContext });
    			tb2.SetBinding(TextBlock.TextProperty, mb2);
    			dt2a.VisualTree = tb2;
    			col2.CellTemplate = dt2a;
    			// SecondCellEditingTemplate
    			DataTemplate dt2b = new DataTemplate();
    			FrameworkElementFactory cb2 = new FrameworkElementFactory(typeof(ComboBox));
    			cb2.SetValue(ComboBox.DisplayMemberPathProperty, ProvincePathValue);
    			cb2.SetBinding(ComboBox.ItemsSourceProperty, new Binding("CurrentProvinces") { Source = grid.DataContext, Mode = BindingMode.OneWay });
    			cb2.SetBinding(ComboBox.SelectedValueProperty, new Binding(ProvinceIDValue) { Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });
    			cb2.SetValue(ComboBox.SelectedValuePathProperty, ProvinceSelectedValue);
    			dt2b.VisualTree = cb2;
    			col2.CellEditingTemplate = dt2b;
    			dg.Columns.Add(col2);
    
    			// Third column
    			DataGridTemplateColumn col3 = new() { Header = "District", Width = 150 };
    			DataTemplate dt3a = new DataTemplate();
    			FrameworkElementFactory tb3 = new FrameworkElementFactory(typeof(TextBlock));
    			MultiBinding mb3 = new MultiBinding() { Mode = BindingMode.OneWay };
    			mb3.Converter = new ConvDistrictID();
    			mb3.Bindings.Add(new Binding(DistrictIDValue));
    			mb3.Bindings.Add(new Binding("Districts") { Source = grid.DataContext });
    			tb3.SetBinding(TextBlock.TextProperty, mb3);
    			dt3a.VisualTree = tb3;
    			col3.CellTemplate = dt3a;
    			// SecondCellEditingTemplate
    			DataTemplate dt3b = new DataTemplate();
    			FrameworkElementFactory cb3 = new FrameworkElementFactory(typeof(ComboBox));
    			cb3.SetValue(ComboBox.DisplayMemberPathProperty, DistrictPathValue);
    			cb3.SetBinding(ComboBox.ItemsSourceProperty, new Binding("CurrentDistricts") { Source = grid.DataContext, Mode = BindingMode.OneWay });
    			cb3.SetBinding(ComboBox.SelectedValueProperty, new Binding(DistrictIDValue) { Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });
    			cb3.SetValue(ComboBox.SelectedValuePathProperty, DistrictSelectedValue);
    			dt3b.VisualTree = cb3;
    			col3.CellEditingTemplate = dt3b;
    			dg.Columns.Add(col3);
    		}
    
    		public class ConvProvinceID : IMultiValueConverter
    		{
    			public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    			{
    				if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue || values[1] == null) return string.Empty;
    				int i = (int)values[0];
    				IList<ProvincesModel> l = (IList<ProvincesModel>)values[1];
    				return i > 0 && (l.Count > 0) ? l[i - 1].ProvinceName : string.Empty;
    			}
    			public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    			{
    				throw new NotImplementedException();
    			}
    		}
    		public class ConvDistrictID : IMultiValueConverter
    		{
    			public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    			{
    				if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue || values[1] == null) return string.Empty;
    				int i = (int)values[0];
    				IList<DistrictsModel> l = (IList<DistrictsModel>)values[1];
    				return (i > 0 && l.Count > 0) ? l[i - 1].DistrictName : string.Empty;
    			}
    			public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    			{
    				throw new NotImplementedException();
    			}
    		}
    	}
    
    
    

    and you get result: x

    If you use Arrow in right of TextBlock and MouseEnterEvent to switch to edit mode you get this result: x