Search code examples
c#wpfbindingdatagridtemplatecolumn

2nd Binding property of ComboBox pointing to its first property binding object


I tried to solve this problem for 1 week, But I couldn't! I was searching and reading many pages, including these:

(www.thomaslevesque.com) [WPF] HOW TO BIND TO DATA WHEN THE DATACONTEXT IS NOT INHERITED

(stackoverflow) How do I use WPF bindings with RelativeSource?

(stackoverflow) WPF - Binding error in DataGridComboboxColumn

(stackoverflow) WPF Error 40 BindingExpression path error: property not found on 'object'

What am I trying to do!?

I have a domain class and a table in SQL LocalDb by name of TermType (you can see its code below) with 5 properties:

  • TermTypeId
  • TypeName
  • Description
  • StartDate
  • EndDate

My app reads TermType table and It supposed to show them inside a DataGrid like this:

enter image description here

But it do not show Start/End date for each term type because It failed in binding StartDate/EndDate properties in right way! and Also I get this error message at Output window (not as exception):

System.Windows.Data Error: 40 : BindingExpression path error: 'StartDate' property not found on 'object' ''MonthName' (HashCode=38847270)'. BindingExpression:Path=StartDate; DataItem='MonthName' (HashCode=38847270); target element is 'ComboBox' (Name=''); target property is 'SelectedIndex' (type 'Int32')

Before I say more! Please check my UserControl xaml file that related to this window:

<UserControl x:Class="PresentationWPF.View.UserPanels.UserControlTermTypeCrud"
             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:local="clr-namespace:PresentationWPF.View.UserPanels"
             xmlns:userPanels="clr-namespace:PresentationWPF.ViewModel.Client.UserPanels"
             xmlns:converters="clr-namespace:PresentationWPF.View.Converters"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             Background="DarkSlateGray">
    <UserControl.Resources>
        <userPanels:TermTypeCrudViewModel x:Key="TermTypeCrudViewModel"/>
        <converters:IndexToMonthConverter x:Key="IndexToMonthConverter"/>
    </UserControl.Resources>

    <Grid DataContext="{StaticResource TermTypeCrudViewModel}">
        <Grid.RowDefinitions>
            <RowDefinition Height="9*"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <DataGrid ItemsSource="{Binding TermTypes}" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
            <!--
            <DataGrid.Resources>
                <userPanels:BindingProxy x:Key="MyProxy" Data="{Binding}"/>
            </DataGrid.Resources>
            -->
            <DataGrid.Columns>
                <DataGridTextColumn Header="Term Name" Binding="{Binding TypeName}" Width="120"/>
                <DataGridTextColumn Header="Description" Binding="{Binding Description}" Width="*"/>

                <DataGridTemplateColumn Header="Date Range" Width="150">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition/>
                                </Grid.ColumnDefinitions>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="*"/>
                                    <RowDefinition Height="*"/>
                                </Grid.RowDefinitions>
                                    <TextBlock  Grid.Column="0" Grid.Row="0" Text="Start: "/>
                                    <ComboBox  Grid.Column="1" Grid.Row="0" 
                                               Style="{StaticResource ComboBoxMonthNamesStyle}"
                                               SelectedIndex="{Binding StartDate,
                                                                Converter={StaticResource IndexToMonthConverter}}"
                                    />

                                    <TextBlock Grid.Column="0" Grid.Row="1" Text="End: "/>
                                    <ComboBox Grid.Column="1" Grid.Row="1" 
                                              Style="{StaticResource ComboBoxMonthNamesStyle}"
                                              SelectedIndex="{Binding EndDate, 
                                                                Converter={StaticResource IndexToMonthConverter}}"
                                    />
                            </Grid>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</UserControl>

I know the problem at ComboBox is in its second property binding SelectedIndex that it try to find out 'StartDate' property inside of MonthName object, instead of TermTypes from DataGrid ItemsSource="{Binding TermTypes}". But I do not know how to do it!? Even I tried to use RelativeSourceinside of SelectedIndex property binding, But I may not use it correctly!!!

If in Visual Studio 2017 I hover mouse over StartDate in ComboBox, It show this message too:

Cannot resolve property 'StartDate' in data context of type 'PresentationWPF.View.UserPanels.TermTypeCrudViewModel'

I even try to use Freezable class (as suggested in first link I put at begging) and as you can see I commented the line of DataGrid.Resources, but even if I try to use it at ComboBox, SelectedIndex still Data property of BindingProxy object do not point to TermTypes.

NOTE: I try to insert month names directly inside ComboBox in xaml (by using ComboBoxItem) and omit Stylebinding like below code and It worked, But still I like to know how to fix the code while I am using Stylebinding in that way:

<ComboBox  Grid.Column="1" Grid.Row="0" 
           SelectedIndex="{Binding StartDate,
                            Converter={StaticResource IndexToMonthConverter}}"
>
    <ComboBoxItem>January</ComboBoxItem>
    <ComboBoxItem>February</ComboBoxItem>
    <ComboBoxItem>March</ComboBoxItem>
    <ComboBoxItem>April</ComboBoxItem>
    <ComboBoxItem>May</ComboBoxItem>
    <ComboBoxItem>June</ComboBoxItem>
    <ComboBoxItem>July</ComboBoxItem>
    <ComboBoxItem>August</ComboBoxItem>
    <ComboBoxItem>September</ComboBoxItem>
    <ComboBoxItem>October</ComboBoxItem>
    <ComboBoxItem>November</ComboBoxItem>
    <ComboBoxItem>December</ComboBoxItem>
</ComboBox>

I will appreciated to tell me how to solve this problem!?

Did I use MVVM and UnitOfWork in the right way!?

Any better suggestion to replace MonthName class or ComboBox Style in App.xaml!?

Or any other issues that may you see in my codes!? Thanks so much in Advanced.

In case you need to see/know about my other related classes and ... here they are:

TermType Class:

namespace Core.BusinessLayer.Domain
{
    public partial class TermType
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public TermType()
        {
            Terms = new HashSet<Term>();
        }

        public int TermTypeId { get; set; }

        public string TypeName { get; set; }

        public string Description { get; set; }

        public DateTime? StartDate { get; set; }

        public DateTime? EndDate { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<Term> Terms { get; set; }
    }
}

(Model) TermTypeCrudService Class:

namespace PresentationWPF.Model
{
    public class TermTypeCrudService : IDisposable
    {
        private readonly UnitOfWork _unitOfWork = new UnitOfWork(new AgsContext());
        public bool IsDbDirty = false;

        public IEnumerable<TermType> GetTermTypes() => _unitOfWork.TermTypes.GetAll();

        public void Dispose()
        {
            if (IsDbDirty)
                _unitOfWork.Complete();
            _unitOfWork.Dispose();
        }
    }
}

(ViewModel) TermTypeCrudViewModel Class:

namespace PresentationWPF.ViewModel.Client.UserPanels
{
    public class TermTypeCrudViewModel : INotifyPropertyChanged
    {
        private readonly TermTypeCrudService _termTypeCrudService = new TermTypeCrudService();

        private ObservableCollection<TermType> _termTypes;
        public ObservableCollection<TermType> TermTypes
        {
            get
            {
                return _termTypes;
            }
            set
            {
                _termTypes = value;
                OnPropertyChanged();
            }
        }

        public TermTypeCrudViewModel()
        {
               TermTypes = new ObservableCollection<TermType>(_termTypeCrudService.GetTermTypes());
        }
        public void Dispose() => _termTypeCrudService.Dispose();

        public event PropertyChangedEventHandler PropertyChanged;
        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

(View - Code behind) class:

namespace PresentationWPF.View.UserPanels
{
    public partial class UserControlTermTypeCrud : IUserPanelNavigation
    {
        private readonly TermTypeCrudViewModel _termTypeCrudViewModel;

        public UserControlTermTypeCrud()
        {
            InitializeComponent();

            _termTypeCrudViewModel = FindResource("TermTypeCrudViewModel") as TermTypeCrudViewModel;
        }

        public ObservableCollection<TermType> TermTypes
        {
            get => (ObservableCollection<TermType>)_termTypeCrudViewModel.TermTypes;
            set => _termTypeCrudViewModel.TermTypes = value;
        }


        public event EventHandler OnNavigateEvent;
        public string Title => "Term Types";
        public UserControl NavigateToPanel { get; set; }

        public void Dispose()
        {
            _termTypeCrudViewModel.Dispose();
        }
    }
}

App.xaml file:

<Application x:Class="PresentationWPF.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:PresentationWPF"
             xmlns:userPanels="clr-namespace:PresentationWPF.ViewModel.Client.UserPanels"
             Startup="Application_Startup">
    <Application.Resources>
        <!--
            Create Month names list to use in ComboBox
        -->
        <userPanels:MonthName x:Key="MonthName" />
        <Style TargetType="ComboBox" x:Key="ComboBoxMonthNamesStyle">
            <Setter Property="DataContext" Value="{StaticResource MonthName}"/>
            <Setter Property="ItemsSource" Value="{Binding MonthNamesCollection}"/>
            <Setter Property="Width" Value="100"/>
        </Style>
    </Application.Resources>
</Application>

MonthName class:

namespace PresentationWPF.ViewModel.Client.UserPanels
{
    public class MonthName 
    {
        private ObservableCollection<string> _monthNamesCollection = new ObservableCollection<string>();

        public ObservableCollection<string> MonthNamesCollection
        {
            get => _monthNamesCollection;
            set => _monthNamesCollection = value;
        }

        public MonthName()
        {
            MonthNamesCollection.Add("January"); //      31 days
            MonthNamesCollection.Add("February"); //     28 days in a common year and 29 days in leap years
            MonthNamesCollection.Add("March"); //        31 days
            MonthNamesCollection.Add("April"); //        30 days
            MonthNamesCollection.Add("May"); //          31 days
            MonthNamesCollection.Add("June"); //         30 days
            MonthNamesCollection.Add("July"); //         31 days
            MonthNamesCollection.Add("August"); //       31 days
            MonthNamesCollection.Add("September"); //    30 days
            MonthNamesCollection.Add("October"); //      31 days
            MonthNamesCollection.Add("November"); //     30 days
            MonthNamesCollection.Add("December"); //     31 days
        }
    }
}

Converter class that I used in ComboBox SelectedIndex:

namespace PresentationWPF.View.Converters
{
    public class IndexToMonthConverter : IValueConverter
    {
        private DateTime _dateTime;

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            // Convert Month in 'value(DateTime)' ==> Index 0 to 11
            if (value is DateTime b)
            {
                _dateTime = b;
                return b.Month - 1;
            }

            return 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            // Convert 'value(int)' 0 to 11 ==> Month number
            if(value is int b)
                return new DateTime(1,b + 1,1);

            return _dateTime;
        }
    }
}

Solution

  • Do not set the DataContext property in a Combobox Style. Doing so breaks any DataContext-based Binding like {Binding StartDate}.

    <Style TargetType="ComboBox" x:Key="ComboBoxMonthNamesStyle">
        <Setter Property="ItemsSource"
                Value="{Binding MonthNamesCollection, Source={StaticResource MonthName}}"/>
        ...
    </Style>