Search code examples
c#wpfbindingdatagridtemplatecolumnimultivalueconverter

IMultiValueConverter receives empty string from bound value when used in a DataGridTemplateColumn


I have a nested bunch of objects in an Observable Collection which is bound to a DataGrid. A IMultiValueConverter is used to gather information from two properties; this works when I do so in a DataGridTextColumn, but fails in a DataGridTemplateColumn. It's a complex situation, so I'll break it down further and post a simplified version of my code.

The nesting of the each list item is as follows: User_Ext class which inherits the User class which has a property of the User_Rank class which in turn has a property of the User class. Unfortunately, this nesting is necessary for the way the program is set up.

There is also a separate list of Rank objects which is bound as the options for a ComboBox in a DataGridTemplateColumn, which will switch the Rank from the item in the ObservableCollection.

The Rank has a boolean property Require_License and the User has a string property License. The idea is the highlight the cell, using the IMultiValueConverter, if the License is blank and the Require_License is true.

I've included both a DataGridTextColumn and a DataGridTemplateColumn in my example code here to more easily demonstrate what is happening.

For the DataGridTextColumn bound to License, the converter fires as soon as I edit the content of either the Rank cell's ComboBox choice or the License text, and all the information carries over.

For the DataGridTemplateColumn bound to License, the converter only fires when I change the ComboBox choice, but not when I edit the License text. On top of that, when the converter catches the ComboBox change, the value for license is an empty string (not an UnsetValue) rather than the cell's content, while the second bound value (the Rank choice) is correct. I should also mention here that any changes being made are correctly updating the items in the ObservableCollection, so that aspect of the binding is working properly.

I've gotten as far as I have with my searches here, but I can't seem to find the solution to this problem.

I apologize in advance if anything is messy or forgotten, but I had to strip down my work's identifying markers and wanted to include as much as possible, since I'm not sure where the issue is occurring. My code is operational, however, if it helps to copy it into a project and test it out. I also apologize if I've been too verbose; this is my first question here and I'm not sure how much wording is appropriate to describe this situation.

As for why I don't just use the functional DataGridTextColumn, there are more things I need to put into place for which I will require the flexibility of the DataGridTemplateColumn.

Here is my XAML:

<Window x:Class="Tool.Transfer"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:src="clr-namespace:Tool"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        >
    <Window.Resources>
        <src:MatchMultiCellColourConverter x:Key="MatchMultiCellColourConverter"/>
    </Window.Resources>
    <Grid>
        <DataGrid ItemsSource="{Binding UserImport, Mode=TwoWay}" AutoGenerateColumns="False">
            <DataGrid.Resources>
                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black"/>
            </DataGrid.Resources>
            <DataGrid.Columns>
                <DataGridTextColumn Header="User" Binding="{Binding User_Code}"/>
                <DataGridComboBoxColumn Header="Rank" DisplayMemberPath="Desc" SelectedValuePath="Code" SelectedItemBinding="{Binding user_Rank.rank}">
                    <DataGridComboBoxColumn.ElementStyle>
                        <Style TargetType="{x:Type ComboBox}">
                            <Setter Property="ItemsSource" Value="{Binding Path=TargetRanks, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
                            <Setter Property="DisplayMemberPath" Value="Desc"/>
                            <Setter Property="Background" Value="White"/>
                        </Style>
                    </DataGridComboBoxColumn.ElementStyle>
                    <DataGridComboBoxColumn.EditingElementStyle>
                        <Style TargetType="{x:Type ComboBox}">
                            <Setter Property="ItemsSource" Value="{Binding Path=TargetRanks, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
                            <Setter Property="DisplayMemberPath" Value="Desc"/>
                        </Style>
                    </DataGridComboBoxColumn.EditingElementStyle>
                </DataGridComboBoxColumn>

                <DataGridTextColumn Header="TextColumn License" Binding="{Binding License}">
                    <DataGridTextColumn.ElementStyle>
                        <Style TargetType="{x:Type TextBlock}">
                            <Style.Setters>
                                <Setter Property="Background">
                                    <Setter.Value>
                                        <MultiBinding Converter="{StaticResource MatchMultiCellColourConverter}">
                                            <Binding Path="License"/>
                                            <Binding Path="user_Rank.rank"/>
                                        </MultiBinding>
                                    </Setter.Value>
                                </Setter>
                            </Style.Setters>
                        </Style>
                    </DataGridTextColumn.ElementStyle>
                </DataGridTextColumn>

                <DataGridTemplateColumn Header="TemplateColumn License">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding License, UpdateSourceTrigger=PropertyChanged}">
                                <TextBox.Style>
                                    <Style TargetType="{x:Type TextBox}">
                                        <Style.Setters>
                                            <Setter Property="Background">
                                                <Setter.Value>
                                                    <MultiBinding Converter="{StaticResource MatchMultiCellColourConverter}">
                                                        <Binding Path="License"/>
                                                        <Binding Path="user_Rank.rank"/>
                                                    </MultiBinding>
                                                </Setter.Value>
                                            </Setter>
                                        </Style.Setters>
                                    </Style>
                                </TextBox.Style>
                            </TextBox>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>

        </DataGrid>
    </Grid>
</Window>

And my C#:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace Tool
{
    public partial class Transfer
    {
        private ObservableCollection<User_Ext> _userImport = null;
        public ObservableCollection<User_Ext> UserImport
        {
            get
            {
                if (_userImport == null)
                {
                    _userImport = new ObservableCollection<User_Ext>();
                }
                return _userImport;
            }
            set { _userImport = value; }
        }

        private ObservableCollection<Rank> _targetRanks = null;
        public ObservableCollection<Rank> TargetRanks
        {
            get
            {
                if (_targetRanks == null)
                {
                    _targetRanks = new ObservableCollection<Rank>();
                }
                return _targetRanks;
            }
            set { _targetRanks = value; }
        }

        public Transfer()
        {
            Rank r1 = new Rank(); r1.Code = "R1"; r1.Desc = "Rank1"; r1.Require_License = false;
            Rank r2 = new Rank(); r2.Code = "R2"; r2.Desc = "Rank2"; r2.Require_License = true;

            User a = new User(); a.User_Code = "A"; a.License = ""; a.user_Rank = new User_Rank(); a.user_Rank.rank = r1;
            User b = new User(); b.User_Code = "B"; b.License = ""; b.user_Rank = new User_Rank(); b.user_Rank.rank = r2;


            TargetRanks.Add(r1); TargetRanks.Add(r2);
            UserImport.Add(new User_Ext(a)); UserImport.Add(new User_Ext(b));

            InitializeComponent();
        }
    }

    public class MatchMultiCellColourConverter : IMultiValueConverter
    {
        #region IValueConverter Members

        public object Convert(object[] value, Type targetRank, object parameter, System.Globalization.CultureInfo culture)
        {
            if (targetRank != typeof(Brush))
                throw new InvalidOperationException("The target must be a Brush");

            bool pass = false;

            if ( value[0] != DependencyProperty.UnsetValue && value[1] != DependencyProperty.UnsetValue)
            {
                String l = (String)value[0];

                Rank r = (Rank)value[1];
                pass = !((l ?? "") == "" && r.Require_License);
            }
            return pass ? Brushes.White : Brushes.Pink;
        }

        public object[] ConvertBack(object value, Type[] targetRank, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotSupportedException();
        }

        #endregion
    }

    public class User_Ext : User, INotifyPropertyChanged
    {
        private bool _isComplete;
        public bool IsComplete
        {
            get { return _isComplete; }
            set
            {
                _isComplete = value;
                NotifyPropertyChanged("IsComplete");
            }
        }

        public User_Ext(User u) : base(u)
        {
            IsComplete = false;
        }

        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
        #endregion
    }

    public class User
    {
        public string User_Code { get; set; }
        public string License { get; set; }
        public User_Rank user_Rank { get; set; }

        public User() { }

        public User(User u) 
        {
            User_Code = u.User_Code;
            License = u.License;
            user_Rank = u.user_Rank;
        }
    }

    public class User_Rank
    {
        public Rank rank { get; set; }
    }

    public class Rank
    {
        public string Code { get; set; }
        public string Desc { get; set; }
        public bool Require_License { get; set; }
    }

}

EDIT 2017-07-25

I've been playing around with it more and I've found that I have the same problem with the DataGridCheckboxColumn. Now, I don't know much about the inner functioning of controls, but this is what I've observed.

-The IMultiValueConverter does indeed see the initial value of the cell.

-When using a DataGridTemplateColumn with a TextBox within, the ItemsSource bound to the DataGrid, UserImport, does get updated when changes are made in the DataGrid. However, the IMultiValueConverter doesn't fire when the bound License changes. It does when the bound user_Rank.rank changes (in a DataGridComboBoxColumn), but even so, the License changes are not reflected.

-If I try to use a DataGridCheckBoxColumn, the same is true.

-If I click a column header, causing a Sort Columns to occur, the IMultiValueConverter will pick up the value of License at the time of the sort, but no updates afterwards.

-If I use a DataGridTemplateColumn with a DatePicker within, I have the same problem as the other cases: the IMultiValueConverter doesn't pick up changes... except for when it does. If I click about madly, inside the text area, on the date picker button, choosing a date in the date picker, clicking on the little space to the right of the date picker button, and clicking away from the box, I've found that sometimes the IMultiValueConverter will fire. Sometimes it's when I click a date in the DatePicker, sometimes it's when I click that space next to the button, sometimes it's when I click on another cell after having clicked on that space next to the button.

So, I have a value updated in the cell, updated in the bound object, yet somehow not being picked up by the IMultiValueConverter, except for under certain circumstances. It's as though there is a third spot the data is being stored. I wonder (again, no inner knowledge of the controls) whether some cell content updates only the Control within the cell but not the cell itself when you click away.

Is it possible that the control within a cell may have it's "Value" measured separately from the cell, until it "Updates" the cell's "Value"? If that were the case, and the controls are updating the bound object without updating the cell, and the IMultiValueConverter is looking at the cell but not the bound object or the Control within the cell... maybe that would be my problem?

Please, someone, tell me how wrong I am, followed by an explanation for this phenomenon. :)

EDIT I've found a solution which I will post.


Solution

  • I've found a solution.

    Although I'm not sure why it was able to properly find user_Rank.rank and not License, since they are bound to the same object, it seems it was getting lost trying to find License.

    If I had it look at it's own content instead, which is bound to the object anyhow, it could carry it properly to the IMultiValueConverter.

    I changed the DataGridTemplateColumn code slightly to do that:

    <DataGridTemplateColumn Header="TemplateColumn License">
        <DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
                <TextBox Text="{Binding License}">
                    <TextBox.Style>
                        <Style TargetType="{x:Type TextBox}">
                            <Style.Setters>
                                <Setter Property="Background">
                                    <Setter.Value>
                                        <MultiBinding Converter="{StaticResource MatchMultiCellColourConverter}">
                                            <Binding Path="Text" RelativeSource="{RelativeSource Self}"/>
                                            <Binding Path="user_Rank.rank"/>
                                        </MultiBinding>
                                    </Setter.Value>
                                </Setter>
                            </Style.Setters>
                        </Style>
                    </TextBox.Style>
                </TextBox>
            </DataTemplate>
        </DataGridTemplateColumn.CellTemplate>
    </DataGridTemplateColumn>
    

    Now IMultiValueConverter picks up each change immediately.

    This can be applied to the other examples I provided. For a DataGridCheckBoxColum, the TargetType I used for the style was DataGridCell, so I used Path="Content.IsChecked" to access the CheckBox.

    I haven't exactly solved the mystery, but I've come up with something so that I can move forward with my program. If anyone has the smarter answer, please feel free to lay it out for us. :)