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
{
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);
}
}
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:
I have created a CustomControl called TagSelect that takes a List 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 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:
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 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.:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (SelectedTags == null) SelectedTags = new List<Tag>(); <- Break here
....
....
FullTagSet = TagData.TagList;
LoadSelections();
}
I have tried all reasonable suggestions I have found in researching this problem through StackOverFlow, CodeProject, GitHub, and any other reference from search engines.
As said earlier, I expected the control to bind correctly in the DataGrid column as it did when the control was used stand-alone.
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 it fail to access the List 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?
With the guidance of @BionicCode and @Clemens, I have solved the problem. The key was to use the PropertyChangedCallBack to manage when the SelectTags List 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 is bound and when I can access it and the PART properties.
Here is what the PropertyChangedCallback method looks like now:
Thank both of you, again, for your interest and guidance.
The completed custom control and its test harness have been updated on github and I will leave it public for a short while.