Click here to Skip to main content
15,887,083 members
Please Sign up or sign in to vote.
1.00/5 (1 vote)
Binding not working when Custom Control is used in a DataGridTemplateColumn

My charge is to provide a control that can be used to select multiple items from a list of items. The items are classification tags. The object is a Tag. It has three properties (ID, Name, Description). Here is the object:
public class Tag : ObservableObject
{

  private int tagID;
  private string tagName;
  private string description;

  #region Properties
  public int TagID
  {
    get => tagID;
		set => SetProperty(ref tagID, value);
  }
  public string Name
  {
    get => tagName; 
	set => SetProperty(ref tagName, value);
  }
  public string Description
  {
    get => description; 
    set => SetProperty(ref description, value);
  }

  public Tag()
  { }
}

I have created a CustomControl called TagSelect that takes a List<tag> into a Dependency Property called SelectedTags. These are the tags that should show as selected, initially. It then builds an observable collection of all possible Tags so a user can select/deselect the relevant tags. It also manages the accuracy of the original SelectedTags list.

The problem I need help with is this. When I test this control by itself (just as an element in a Grid) it works without issue. The original goal, though, was for use in a DataGridColumn. When I built a DataGridTemplateColumn and used the TagSelect Control in the CellEditingTemplate, the DP for the SelectedTags is null. I know it is null, because it shows (== null) inside TagSelect control at a breakpoint in the OnApplyTemplate method.

I have built a small project to drive the testing of this control (TagBindExample). It can be accessed at
https://github.com/hardoverton/TagBindExample
Some relevant pieces of the Solution:
Here is how the control is used when standing alone in a grid:
<cc:TagSelect Width="175"
					SelectedTags="{Binding SomeTags}" />
<cc:TagSelect Width="175"
			  SelectedTags="{Binding OtherTags}" />

Here is how the DataGrid is defined:
<DataGrid x:Name="TestGrid"
				Grid.Row="3"
				AutoGenerateColumns="False"
				ItemsSource="{Binding DataEntries}"
				Margin="80,0,80,0"
				Grid.ColumnSpan="2">
  <DataGrid.Columns>
    <DataGridTextColumn Width="40"
						Binding="{Binding ID}"
						Header="ID" />
    <DataGridTextColumn Width="200"
						Binding="{Binding Name}"
						Header="Name" />
    <DataGridTemplateColumn Width="175"
							Header="Tags">
      <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
		  <TextBlock Text="{Binding TagString}" 
					Width="175"/>
		</DataTemplate>
	  </DataGridTemplateColumn.CellTemplate>
	  <DataGridTemplateColumn.CellEditingTemplate>
	    <DataTemplate>
		  <cc:TagSelect Width="175"
						SelectedTags="{Binding Tags}" />
		</DataTemplate>
	  </DataGridTemplateColumn.CellEditingTemplate>
	</DataGridTemplateColumn>
  </DataGrid.Columns>
</DataGrid>

The test data for the DataGrid is an ObservableCollection<dataentry> DataEntries. Here is the test data:
SomeTags List:
  3 - Boogie
  1 - Accord

OtherTags List:
  5 - Christmas Gifts
  6 - Data Science
  7 - Envoy

DataEntries Collection:
  0 - First Person, Tags:
        7 - Envoy
        2 - Bathroom Project
  1 - Second Person, Tags:
        5 - Christmas Gifts
        1 - Accord
        3 - Boogie
        8 - Fund-House
  2 - Third Person, Tags:
        5 - Christmas Gifts
        6 - Data Science
        7 - Envoy

Here is the definition of DataEntry:
public class DataEntry : ObservableObject
{
	private int id;
	public int ID
    {
      get => id;
	  set => SetProperty(ref id, value);
    }

    private string? name;
    public string? Name
    {
	  get => name;
	  set => SetProperty(ref name, value);
    }

    private List<Tag> tags;
    public List<Tag> Tags
    {
			get => tags;
			set => SetProperty(ref tags, value);
    }

  private string tagString;
  public string TagString
  {
    get => tagString;
	set => SetProperty(ref tagString, value);	
  }

public DataEntry()
{
  tags = new List<Tag>();
}

Here is the definition of the SelectedTags Dependency Property in the TagSelect control:
C#
public List<Tag> SelectedTags
	{
		get { return (List<Tag>)GetValue(SelectedTagsProperty); }
		set { SetValue(SelectedTagsProperty, value); }
	}
	public static readonly DependencyProperty SelectedTagsProperty =
			DependencyProperty.Register("SelectedTags", typeof(List<Tag>), typeof(TagSelect),
					new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

Only when used in the DataGridTemplateColumn, the DP is null. I do not understand why. For some reason it is not seeing the DataGridRow’s DataContext. The data for the cell in the record is a List<tag> just as when it is used stand-alone. I know it is null for each of the three test records because I see that in the following location for each instance of the control. There are no binding failures shown.:
C#
public override void OnApplyTemplate()
{
  base.OnApplyTemplate();

  if (SelectedTags == null) SelectedTags = new List<Tag>();  <- break here
....
....

  FullTagSet = TagData.TagList;
  LoadSelections();
}


What I have tried:

I have tried all reasonable suggestions I have found in researching this problem through StackOverFlow, CodeProject, GitHub, and any other reference from search engines.

I added the TagString binding to use in the CellTemplate just to see if I was able to reference the DataGridRow properly. It works as expected.

What am I doing wrong in the CellEditingTemplate that makes me fail to
access the List<tag> Tags in each record?

Am I trying to do something a DataGridTemplateColumn will not allow?

If so, is there another approach to the requirement I should consider?
Posted
Updated 6-Mar-24 12:11pm
v2
Comments
[no name] 2-Mar-24 16:12pm    
Who or what is "Tag"? As in: a List of "type" "Tag".
hardover 2-Mar-24 18:53pm    
just a classification item (observable)
public int TagID
public string Name
public string Description
[no name] 2-Mar-24 19:54pm    
You're not binding to any "properties". In fact, you don't have any properties; you have "fields".
hardover 4-Mar-24 16:40pm    
Gerry, I think I see why you might think I don't have properties. Maybe I should have shown a lot more code here even while being encouraged to minimize it. My bad.
If you are still inclined to help, please take a look at my github repo to see all of the code for the solution.

List is not a bindable collection. You need to use the ObservableCollection<T> Class (System.Collections.ObjectModel) | Microsoft Learn[^].

Here is a working solution for a similar question. Create a new project and add this code:

1. Classes:
C#
public class OutputColumn : ObservableObject
{
    private string spaltenname;
    private bool isChecked;

    public string Spaltenname
    {
        get => spaltenname;
        set => Set(ref spaltenname, value);
    }

    public bool IsChecked
    {
        get => isChecked;
        set => Set(ref isChecked, value);
    }

    public ObservableCollectionEx<Tag> Tags { get; set; }
}

public class Tag : ObservableObject
{
    private string name;
    private bool isChecked;

    public string Name
    {
        get => name;
        set => Set(ref name, value);
    }

    public bool IsChecked
    {
        get => isChecked;
        set => Set(ref isChecked, value);
    }
}

public class ObservableCollectionEx<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    private int itemChangeCount;

    public int ItemChangeCount
    {
        get => itemChangeCount;
        set
        {
            itemChangeCount = value;
            OnPropertyChanged(new PropertyChangedEventArgs(nameof(ItemChangeCount)));
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            foreach (T item in e.NewItems!.Cast<T>())
            {
                item.PropertyChanged += Item_PropertyChanged;
            }
        }
        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            foreach (T item in e.NewItems!.Cast<T>())
            {
                item.PropertyChanged -= Item_PropertyChanged;
            }
        }
    }

    private void Item_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        ItemChangeCount += 1;
    }
}

public abstract class ObservableObject : INotifyPropertyChanged
{
    public void Set<TValue>(ref TValue field, TValue newValue, [CallerMemberName] string propertyName = "")
    {
        if (!EqualityComparer<TValue>.Default.Equals(field, default) && field!.Equals(newValue))
            return;

        field = newValue;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

2. Helper converter for displaying a list of tags in a DataGrid cell:
C#
public class SummaryConverter : IMultiValueConverter
{
    public object? Convert(
        object[] values, 
        Type targetType, 
        object parameter, 
        CultureInfo culture)
    {
        if (values.Length < 2)
            return null;

        if (values
            .Any(x => x == DependencyProperty.UnsetValue))
            return null;

        bool isChecked = (bool)values[0];
        IEnumerable<Tag> TagItems = (IList<Tag>)values[1];

        List<string> tags = new()
            { isChecked
                ? "checked "
                : "unchecked " };

        tags.AddRange(TagItems
            .Where(tag => tag.IsChecked)
            .Select(tag => tag.Name));

        return string.Join(", ", tags);
    }

    public object[] ConvertBack(
        object value,
        Type[] targetTypes,
        object parameter,
        CultureInfo culture)
        => throw new NotImplementedException();
}

3. The MainWindow code-behind:
C#
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        InitData();
    }

    private Random random = new();

    public ObservableCollection<OutputColumn> OutputColumns { get; set; }
        = new();

    void InitData()
    {
        for (int i = 0; i < 10; i++)
        {
            OutputColumn item = new()
            {
                Spaltenname = $"Item {i + 1}",
                IsChecked = random.Next(0, 2) == 1,
                Tags = new()
                {
                    new()
                    {
                        Name = "Asset Type",
                        IsChecked = random.Next(0, 2) == 1
                    },
                    new()
                    {
                        Name = "Service Tag",
                        IsChecked = random.Next(0, 2) == 1
                    },
                    new()
                    {
                        Name = "Item Number",
                        IsChecked = random.Next(0, 2) == 1
                    },
                    new()
                    {
                        Name = "System Brand",
                        IsChecked = random.Next(0, 2) == 1
                    },
                    new()
                    {
                        Name = "Description",
                        IsChecked = random.Next(0, 2) == 1
                    },
                }
            };

            OutputColumns.Add(item);
        }
    }
}

4. The MainWindow Xaml:
XML
<Window x:Class="WpfDataGridComboBoxColumn.MainWindow"
        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:WpfDataGridComboBoxColumn"
        mc:Ignorable="d"
        x:Name="Window"
        Title="MainWindow" Height="450" Width="800">
    <DataGrid x:Name="DataGrid" ItemsSource="{Binding ElementName=Window, Path=OutputColumns}"
              AutoGenerateColumns="False">
        <DataGrid.Resources>
            <local:SummaryConverter x:Key="StaticSummaryConverter" />
        </DataGrid.Resources>
        <DataGrid.Columns>
            <DataGridCheckBoxColumn
                Binding="{
                    Binding IsChecked,
                    UpdateSourceTrigger=PropertyChanged}"
                Header="IsChecked"/>
            <DataGridTextColumn Binding="{Binding Spaltenname}"
                                Header="Name"/>
            <DataGridTemplateColumn Header="AdditionsSpalten">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <ComboBox
                            ItemsSource="{Binding Path=Tags}"
                                                  DisplayMemberPath="Name">
                            <ComboBox.ItemTemplate>
                                <DataTemplate>
                                    <StackPanel Orientation="Horizontal">
                                        <CheckBox 
                                            IsChecked="{Binding IsChecked,
                                                UpdateSourceTrigger=PropertyChanged}" />
                                        <TextBlock Text="{Binding Name}" />
                                    </StackPanel>
                                </DataTemplate>
                            </ComboBox.ItemTemplate>
                        </ComboBox> 

                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
            <DataGridTemplateColumn Header="Summary">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock>
                            <TextBlock.Text>    
                                <MultiBinding
                                    Converter="{StaticResource StaticSummaryConverter}">
                                    <Binding Path="IsChecked" />
                                    <Binding Path="Tags"/>
                                    <Binding Path="Tags.ItemChangeCount"/>
                                </MultiBinding>
                            </TextBlock.Text>
                        </TextBlock>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>

    </DataGrid>
</Window>

Compile, set breakpoints, and run to see how the binding system works.
 
Share this answer
 
Comments
hardover 7-Mar-24 16:21pm    
Thank you for looking at my issue. I will certainly study what you have presented here as soon as I can; most likely over the weekend. Your note at the beginning, however, must mean more that it seems. My sample solution proves that List<> binds to a dependency property without issue when the control is used stand-alone. Would you expound on your point there for me when you can? Thank you.
Graeme_Grant 7-Mar-24 16:35pm    
In your DataEntry class, the binding is only triggered when the list is assigned to the property, not when the collection changes. So whilst it appears that it works, any changes to the list will not cause the DataGrid to refresh the cell as no notification in the binding system is received. The sample that I have given you fixes this issue. It also shows how to solve your key issue with binding to a collection, the Tags property in your DataEntry class.

It will only take a couple of minutes to create a sample project, then copy & paste the code, then run. I strongly recommend that you run the code above, then ask your questions if you are struggling.
hardover 12-Mar-24 17:57pm    
I understand that the binding is triggered when the list is assigned to the property; and that any subsequent changes will not be noticed. In this particular case, however, I am only using the List<> internally in the control. I AM using an ObservableCollection<> for the presentation and updating of the interface.

After going through ComboBox example, I see that you have assumed I needed to be educated on the use of ObservableCollection/INotifyPropertyChanged/…; I do understand these things. While I appreciate your time to offer this information, it is not related to the question I have raised here.
I have solved the problem. See solution posted.
Graeme_Grant 12-Mar-24 19:02pm    
"I AM using an ObservableCollection<> for the presentation and updating of the interface."
With respect, the code posted in the question does not use it, hence my post. I cannot see your screen and codebase, so I can only base my response on what you share and my extensive experience with WPF.

Your solution is still binding to a List<> and will not see any changes to it's contents as the List<> does not implement the INotifyCollectionChanged Interface (System.Collections.Specialized) | Microsoft Learn[^] that is required for Data Binding.

Whilst it may appear that you have solved your problem, you still have this outstanding issue to address.
hardover 12-Mar-24 19:09pm    
Appreciate your concern, but I said somewhere in here that I am managing the changes to the visible list back into the "selected" List. I have tested this and it works nicely. You might say this is unnecessary work due to not using an ObservableCollection, but the Tag does not have the needed Selected Property for the multiselect list and the Tag can't be changed.
With guidance from others, I have solved the problem. The key was to use the PropertyChangedCallBack to manage when the SelectTags List<tag> was bound.

In summary, the solution involves capturing the point at which the actual List<> binding takes place coupled with the point at which the PART properties can be set. This is further complicated by the fact that I am attempting to have one solution that can be used in two situations: as a stand-alone control; and as a cell in a DataGridRow. (I think that this twofold objective is a complicating factor and should probably force a discussion about having two separate controls.)

Detailed study and documentation of when these things could be done resulted in the use of two flags: TemplateApplied & PartsLoaded. Using both I can effectively determine when the SelectTags List<tag> is bound and when I can access it and the PART properties.

Here is what the PropertyChangedCallback method looks like now:
public static readonly DependencyProperty SelectedTagsProperty =
DependencyProperty.Register("SelectedTags", 
typeof(List<Tag>), 
typeof(TagSelect),
				new FrameworkPropertyMetadata(null, 
					FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
					OnSelectedTagsChanged));

private static void OnSelectedTagsChanged(DependencyObject d, 
					DependencyPropertyChangedEventArgs e)
{
	TagSelect ts = (TagSelect)d;
	if (ts != null) 
		ts.SelectedTagsChanged();
}

void SelectedTagsChanged()
{
	if (SelectedTags != null && SelectedTags.Count != 0)
	{
		if (TemplateApplied)
		{
			if (!PartsLoaded)
				LoadPartsProperties();
			LoadSelections();
		}
	}
}

The completed custom control and its test harness have been updated on github and I will leave it public for a short while.
 
Share this answer
 
Comments
Graeme_Grant 12-Mar-24 20:36pm    
I've downloaded your repo and had a look at your project. Let me show you the issue that I highlight...

You have 2 x TagSelect custom controls outside of your DataGrid. You bind the SelectedTags property for each to a List<Tag> collection. The property that exposes these fires a RaisePropertyChanged event if a new collection is assigned. However, when you make selections in your popup, how do you respond to changes to the selected list collection? a List<> has no events for changes... So, if your control is used outside of your DataGrid, you have no way of handling any changes to the selected list.

The reason why it appears to work is due to the internal use of the ObservableCollection within the custom control.

Tis issue is also in your DataGrid. It is easy to demonstrate. Bind the custom control to a collection in your model (which you have done), and bind that property to another column in your DataGrid you will not see any changes. The easiest way to see this is to add the following column to your DataGrid:
<DataGridTextColumn Binding="{Binding Tags.Count}"></DataGridTextColumn>

When you make changes to your TagSelect the count does not change.

If you want to see a list of your tags in another column, look at how I use a converter to do it.

So, please, take another look at what I posted. It is a simple fix.
hardover 13-Mar-24 15:42pm    
I realize that something like a realtime count will not stay current. I don't need to see anything like that in the interface. What I am concerned with is the integrity of the SelectedTags List<>. I am managing that through the routines:
OnSelectionChanged (of the internal collection)
AddSelections
RemoveSelections
Graeme_Grant 13-Mar-24 21:13pm    
You totally missed the point. Your selected tags is not correctly bound in your model. But that is your choice to do so.
hardover 14-Mar-24 18:02pm    
Not the place for arguments, but it is in fact bound and I have tested before and after list selection to confirm it.

Again thanks for your interest and energy. I am going this way.
Graeme_Grant 14-Mar-24 21:06pm    
Please watch your tone. You asked for help. I have pointed out a key issue. You have chosen not to take the advise, that is your choice.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900