Click here to Skip to main content
16,020,261 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I can not work out the binding for UserControl that is in a DataGridTemplateColumn of DataGrid. I built this example to demonstrate the problem.

The UC here is trivial and does nothing meaningful. But eventually I want a UC that does. It will need to take a bound List<t> and use it. That is why this demo tries to bind a List.

Notice that the UC works as planned when it is used on the MainWindow but does not when used in the DataGrid. When debugging you can see that the List (ListItems) is empty in each row. so it makes me think the binding is not working.
There are no binding failure notices

Image of the program output: Program Output

While it is running, observing the DataContext shows:
at first call to UC with NameList
DataContext is BindingTest.MainWindowViewModel

when the UC is called from the DataGrid, the DataContext has changed to DataRecord
For Each row:
DataContext is BindingTest.DataRecord
DataContext is BindingTest.DataRecord
DataContext is BindingTest.DataRecord

All of this seems right.

The problem is: Why does the binding for the DataGrid row not work?

Am I trying to do something that is not possible? If so, why?

Is this something that must be done in a Custom Control instead? Or another way?

Code Follows
The MainWindowViewModel contains:
private List<string> nlist;
 public List<string> NameList
 {
   get { return nlist; }
   set
   {
     nlist = value;
     RaisePropertyChanged();
   }
 }
 private ObservableCollection<DataRecord> gridData;
 public ObservableCollection<DataRecord> GridData
 {
   get { return gridData; }
   set
   {
     gridData = value;
     RaisePropertyChanged();
   }
 }

 public string HeaderLabel { get; set; }

DataRecord is:
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<string> ListItems { get; set; }

BindList.Xaml:
<UserControl
	x:Class="BindingTest.BindList"
	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:local="clr-namespace:BindingTest"
	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
	d:DesignHeight="450"
	d:DesignWidth="1800"
	mc:Ignorable="d">
	<Grid Margin="10,0,0,0">
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="auto" />
			<ColumnDefinition Width="auto" />
		</Grid.ColumnDefinitions>
		<Grid.RowDefinitions>
			<RowDefinition Height="30" />
		</Grid.RowDefinitions>
		<Border
			BorderThickness="1"	
			BorderBrush="Red"
			Grid.Column="0"
			Width="70"
			Height="30">
			<TextBlock
				x:Name="PART_Count"
				Margin="4"/>
		</Border>
		<Border
			BorderThickness="1"				
			BorderBrush="Red"
			Grid.Column="1"
			Width="auto"
			Height="30">
			<TextBlock
				x:Name="PART_List" 
				Margin="4"/>
		</Border>
	</Grid>
</UserControl>

BindList code behind:
public partial class BindList : UserControl
{
  public List<string> ItemsList
  {
    get { return (List<string>)GetValue(ItemsListProperty); }
    set { SetValue(ItemsListProperty, value); }
  }
  public static readonly DependencyProperty ItemsListProperty =
      DependencyProperty.Register("ItemsList",
        typeof(List<string>),
        typeof(BindList));

  public static readonly DependencyProperty BoundDataContextProperty =
    DependencyProperty.Register("BoundDataContext",
      typeof(object),
      typeof(BindList),
      new PropertyMetadata(null, OnBoundDataContextChanged));

  public BindList()
  {
    this.SetBinding(BoundDataContextProperty, new Binding());
    ItemsList = new List<string>();
    InitializeComponent();
    Loaded += new RoutedEventHandler(BindList_Loaded);
  }


  void BindList_Loaded(object sender, RoutedEventArgs e)
  {
    ProcessList();
  }

  private static void OnBoundDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    Debug.Print("BindList DataContext: {0}", e.NewValue.ToString());
  }

  private void ProcessList()
  {
    string listing = "";
    string sep = " | ";
    char[] trimChars = { ' ', '|' };

    if (ItemsList != null)
    {
      foreach (string s in ItemsList)
      {
        listing += s + sep;
      }
      listing = listing.Trim(trimChars);

      PART_Count.Text = string.Format("{0} Items: ", ItemsList.Count);
      PART_List.Text = listing;
    }
  }
}

MainWindow XAML:
<Window
	x:Class="BindingTest.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:local="clr-namespace:BindingTest"
	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
	Title="MainWindow"
	Width="1200"
	Height="650"
	mc:Ignorable="d">
	<Grid>
		<Grid.RowDefinitions>
			<RowDefinition Height="50" />
			<RowDefinition Height="100" />
			<RowDefinition Height="50" />
			<RowDefinition Height="200" />
			<RowDefinition Height="*" />
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="200" />
			<ColumnDefinition Width="*" />
		</Grid.ColumnDefinitions>
		<Label
			Grid.ColumnSpan="2"
			Margin="10,0,0,0"
			HorizontalAlignment="Left"
			VerticalAlignment="Center"
			Content="{Binding HeaderLabel}"
			FontSize="24"
			FontWeight="Bold" />
		<TextBlock
			Grid.Row="1"
			Grid.Column="0"
			HorizontalAlignment="Center"
			VerticalAlignment="Center"
			FontSize="12"
			FontWeight="Bold" 
			Text="ListView Showing NameList: "
			TextWrapping="Wrap" />
		<ListView
			Grid.Row="1"
			Grid.Column="1"
			Width="150"
			FontSize="14"
			Margin="10,0,0,0"
			HorizontalAlignment="Left"
			ItemsSource="{Binding NameList}" />
		<TextBlock
			Grid.Row="2"
			Grid.Column="0"
			HorizontalAlignment="Center"
			VerticalAlignment="Center"
			FontSize="12"
			FontWeight="Bold" 
			Text="BindList UC Showing NameList: "
			TextWrapping="Wrap" />
		<local:BindList
			Grid.Row="2"
			Grid.Column="1"
			FontSize="14"
			VerticalAlignment="Center"
			ItemsList="{Binding NameList}" />
		<TextBlock
			Grid.Row="3"
			HorizontalAlignment="Center"
			VerticalAlignment="Center"
			FontSize="12"
			FontWeight="Bold"
			Text="DataGrid with BindList UC in Rows: "
			Padding="5"
			TextWrapping="Wrap" />
		<DataGrid
			Grid.Row="3"
			Grid.Column="1"
			FontSize="14"
			AutoGenerateColumns="False"
			ItemsSource="{Binding GridData}">
			<DataGrid.Columns>
				<DataGridTextColumn
					Width="30"
					Binding="{Binding ID}"
					Header="ID" />
				<DataGridTextColumn
					Width="150"
					Binding="{Binding Name}"
					Header="Name" />
				<DataGridTextColumn
					Width="150"
					Binding="{Binding Description}"
					Header="Description" />
				<DataGridTemplateColumn 
					Width="390" 
					Header="List Items">
					<DataGridTemplateColumn.CellTemplate>
						<DataTemplate>
							<local:BindList ItemsList="{Binding DataContext.ListItems,
								RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGridRow}}" />
						</DataTemplate>
					</DataGridTemplateColumn.CellTemplate>
				</DataGridTemplateColumn>
			</DataGrid.Columns>
		</DataGrid>
	</Grid>
</Window>

MainWindow code behind:
public MainWindow()
{
  InitializeComponent();
  vm = new MainWindowViewModel();
  DataContext = vm;
}


What I have tried:

I have changed the List<t> to ObservableCollection<t> althought I don't need in this case
just so see if there is a difference. There is not.

Binding statement tries in DataGrid:
ItemsList="{Binding Path=ListItems}"
ItemsList="{Binding Path=GridData.ListItems}"
ItemsList="{Binding Path=DataRecord.ListItems}"
ItemsList="{Binding Path=DataContext.ListItems}"
ItemsList="{Binding DataContext.ListItems,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGridRow}}"
Posted
Updated 30-Aug-22 11:35am
Comments
[no name] 23-Aug-22 7:47am    
At some point, I use a ListView to display (a collection of) user controls instead of trying to stuff a user control into a DataGrid column. If you think about it, a DataGrid row is just another user control (container); except it comes with baggage.
Graeme_Grant 23-Aug-22 8:06am    
Or a converter to show a string of joined items... I've given him an example of a list in a cell... I agree, a UserControl is overkill.
hardover 23-Aug-22 14:59pm    
@salty06: Sorry I don't understand your point. This example is just a proof of concept for using a UC in a DataGridRowColumn. I need to understand why it works on the MainWindow and not in the DataGridTemplateColumn. Any ideas?

[no name] 23-Aug-22 18:23pm    
I get your point ... my point is that there are cleaner ways and nobody wants to go through pointless code. You show "code fragments"; e.g. can't tell if the uc's implement IPropertyChanged. All your "question" does is create more questions about something no one should have much interest in. (bad habits)
hardover 24-Aug-22 16:21pm    
OK. Thanks for input.

Have you checked your binding?

Here is a quick test showing the binding working:

1. Model:
C#
public class DataRecord
{
    public int ID { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
    public ObservableCollection<string>? ListItems { get; set; }
}

2. Code-behind
C#
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        GridData.Add(new()
        {
            ID = 1,
            Name = "Widget 1",
            Description = "This is a widget of #1",
            ListItems = new() { Tags[0], Tags[1], Tags[2], Tags[3], }
        });

        GridData.Add(new()
        {
            ID = 1,
            Name = "Widget 2",
            Description = "This is a widget of #2",
            ListItems = new() { Tags[4], Tags[5], Tags[6], Tags[7], }
        });

        GridData.Add(new()
        {
            ID = 1,
            Name = "Widget 3",
            Description = "This is a widget of #3",
            ListItems = new() { Tags[3], Tags[5], Tags[7], Tags[9], }
        });
    }

    public ObservableCollection<string> Tags { get; } = new()
    {
        "Tag 1", "Tag 2", "Tag 3", "Tag 4", "Tag 5",
        "Tag 6", "Tag 7", "Tag 8", "Tag 9", "Tag 10",
    };

    public ObservableCollection<DataRecord> GridData { get; } = new();
}

3. View:
XML
<Window x:Class="WpfDataGridCellList.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"
        mc:Ignorable="d" x:Name="Window1"
        Title="MainWindow" Height="450" Width="800">

    <Grid DataContext="{Binding ElementName=Window1}">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <DataGrid AutoGenerateColumns="False"
                  ItemsSource="{Binding GridData}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="ID"
                                    Binding="{Binding ID}" />
                <DataGridTextColumn Header="Name"
                                    Binding="{Binding Name}" />
                <DataGridTextColumn Header="Description"
                                    Binding="{Binding Description}" />
                <DataGridTemplateColumn Header="ListItems">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <ItemsControl
                                ItemsSource="{Binding ListItems}">
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <StackPanel
                                            Orientation="Horizontal"/>
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate>
                                        <TextBlock Text="{Binding}"
                                                   Padding="0 0 10 0"/>
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
        
    </Grid>
</Window>

When run, you can see the ListItems for each row.

[UPDATE #1]

As mentioned above, you need to check that you have the collection binding in the DataGridTemplateColumn. The above code does just that.

For the UserControl, binding to a collection is simple. I'm going for a more lightweight Custom Control but using a Label control for a view only custom control.:
C#
public class TagItemsControl : Label
{
    private static readonly Type ctrlType = typeof(TagItemsControl);

    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register(
            nameof(ItemsSource), // property name
            typeof(IEnumerable), // data type
            ctrlType,            // control type
            new PropertyMetadata(null,
                OnItemsSourceChanged));  // Handle changes to the collection

    public IEnumerable? ItemsSource
    {
        get => GetValue(ItemsSourceProperty) as IEnumerable;
        set => SetValue(ItemsSourceProperty, value);
    }

    private static void OnItemsSourceChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        if (d.TryCast(out TagItemsControl? ctrl))
        {
            Binding binding = new Binding(nameof(ItemsSource)) { Source = ctrl };
            // do something here
        }
    }
}

Any template can be applied to the TagItemsControl custom control. I have a simple Template, however requires a IValueConverter to join all of the items being viewed:
C#
public class ListToCsvConverter : IValueConverter
{
    public object Convert(object? value, Type targetType,
        object parameter, CultureInfo culture)
        => value is null
            ? "[No items]"
            : string.Join(", ", (IEnumerable<string>)value);

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

Now we can update the view:
XML
<Window x:Class="WpfDataGridCellList.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:WpfDataGridCellList"
        mc:Ignorable="d" x:Name="Window1"
        Title="MainWindow" Height="450" Width="800">

    <Window.Resources>
        <local:ListToCsvConverter x:Key="ListToCsvConverter" />
        <ControlTemplate x:Key="TagItemsControlTemplate"
                         TargetType="{x:Type local:TagItemsControl}">
            <Label Content="{TemplateBinding ItemsSource,
                Converter={StaticResource ListToCsvConverter}}" />
        </ControlTemplate>
    </Window.Resources>

    <Grid DataContext="{Binding ElementName=Window1}">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <DataGrid AutoGenerateColumns="False"
                  ItemsSource="{Binding GridData}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="ID"
                                    Binding="{Binding ID}" />
                <DataGridTextColumn Header="Name"
                                    Binding="{Binding Name}" />
                <DataGridTextColumn Header="Description"
                                    Binding="{Binding Description}" />
                <DataGridTemplateColumn Header="ListItems">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <local:TagItemsControl
                                ItemsSource="{Binding ListItems}"
                                Template="{StaticResource
                                TagItemsControlTemplate}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
        
    </Grid>
</Window>

When run, you can see the ListItems for each row using the TagItemsControl custom control.

If you don't like my template using a Label control and the ListToCsvConverter to format the layout of the ListItems, then you can change it - totally customizable!

Hope this helps!

[edit #1]

If required, here is the helper extension:
C#
public static class DependencyObjectExtension
{
    public static bool TryCast<TElement>(this DependencyObject dObj,
        out TElement? element)
        where TElement : UIElement
    {
        element = dObj as TElement;
        return element != null;
    }
}

[UPDATE #2]

A Label is extends the ContentControl. The UserControl also extends the ContentControl. You can check this using Peek Definition. I could have used that as my base class for the custom TagItemsControl.

The UserControl will automatically set the DataContext to the parent control's DataContext, so the Properties of the Class contained in the row are exposed to the UserControl binding. You do not require a Dependancy Property. I used a Dependancy Property in a custom control to show how to implement it.

So, here is a simple UserControl version:
XML
<UserControl x:Class="WpfDataGridCellList.TagItemsUserControl"
             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:WpfDataGridCellList">

    <UserControl.Resources>
        <local:ListToCsvConverter x:Key="ListToCsvConverter" />
    </UserControl.Resources>

    <Label Content="{Binding ListItems,
                Converter={StaticResource ListToCsvConverter}}" />
</UserControl>
NOTE: I am not setting the DataContext as it is inherited.

You do not require anything in the Code-Behind (see explanation above)
C#
using System.Windows.Controls;

namespace WpfDataGridCellList;

public partial class TagItemsUserControl : UserControl
{
    public TagItemsUserControl() => InitializeComponent();
}

And finally using the UserControl:
XML
<DataGrid Grid.Row="1" AutoGenerateColumns="False"
          ItemsSource="{Binding GridData}">
    <DataGrid.Columns>
        <DataGridTextColumn Header="ID"
                            Binding="{Binding ID}" />
        <DataGridTextColumn Header="Name"
                            Binding="{Binding Name}" />
        <DataGridTextColumn Header="Description"
                            Binding="{Binding Description}" />
        <DataGridTemplateColumn Header="ListItems">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <local:TagItemsUserControl />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

I have set the DataGrid in a second Row, so use this on the Grid:
XML
<Grid.RowDefinitions>
    <RowDefinition Height="*" />
    <RowDefinition Height="*" />
</Grid.RowDefinitions>


With the CustomControl, I only use a Dependancy Property for the binding data source so as to keep the CustomControl generic - ie: can be used with other collections and layout templates. The issue with the UserControl version, is that it is very specific/restrictive, both the binding data source and layout. It is "best practice" to keep it as generic as possible.

Enjoy!
 
Share this answer
 
v7
Comments
hardover 23-Aug-22 15:49pm    
@Graeme_Grant: Thank you for you effort here, but what you are doing here misses my problem. As I said
"The UC here is trivial and does nothing meaningful. But eventually I want a UC that does. It will need to take a bound List<t> and use it. That is why this demo tries to bind a List."

I have checked my bindings. I DO want to do this with a usercontrol.

"Notice that the UC works as planned when it is used on the MainWindow but does not when used in the DataGrid."

Can we just focus on why it fails in the DataGridRow?

The solution code can be found at: https://github.com/hardoverton/BindingTest
Graeme_Grant 25-Aug-22 8:44am    
Did the update solve your problem? If yes, please flag solution as answered.
hardover 25-Aug-22 16:18pm    
I implemented your update in your first response code that I had working. It works as advertised. The differences in what you are doing here and what I have are (1) the base class for the control is Label, where mine is UserControl, (2) and used the UserControl partial class structure to have XAML to work with in the UC. Yours works as I had expected mine to work. Do you think these differences contribute to my problem? There are bindings being created in mine, but the lists are empty when used in the DataGrid?
Graeme_Grant 25-Aug-22 21:18pm    
What I am showing is that you don't always need to use a UserControl as a CustomControl, you can extend an existing control.

[comment moved into the solution - Update #2]

Solution updated, see above...
OK, after working through some changes one at a time, I have fixed this problem. The single thing that made the difference was the fact that I was initializing ItemsList in the Constructor
public BindList()
{
  //ItemsList = new List<string>();
  InitializeComponent();
  Loaded += new RoutedEventHandler(BindList_Loaded);
}


Comment out that Line and everything works. Can't explain why it works when not in the DataGrid, though. I was following other recommendations from texts and courses in doing that, but it now makes sense that doing it should wipe out whatever list is bound to it. Still would like to know when to expect that a bound item is initialized.
Many Thanks to Graeme_Grant for working through this with me.
 
Share this answer
 

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