Search code examples
c#wpfbindingdatagriddatagridcomboboxcolumn

Can you bind a complex type in a DataGridComboBoxColumn in a DataGrid in WPF?


So this one I am curious on as I may have to change my code base if I cannot get the data right. I was hoping a binding expert on WPF has had something similar and knew how to do it. I was following this guide, http://wpfthoughts.blogspot.com/2015/04/cannot-find-governing-frameworkelement.html, for binding a value in a list that is shown in datagrid to a combobox. Works great, if your property in the collection of objects is a primitive type. If it is complex not so much. I also want it to update the property when it changes implementing INotifyPropertyChanged.

Feel free to download the source code for easier reference: https://github.com/djangojazz/ComboBoxInDataGridViewWPF

BaseViewModel(just for INotifyPropertyChanged reuse):

public abstract class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
      PropertyChangedEventHandler handler = this.PropertyChanged;
      if (handler != null)
      {
        var e = new PropertyChangedEventArgs(propertyName);
        handler(this, e);
      }
    }
}

Essentially I have models as such:

public class Type
{
    public Type(int typeId, string typeName)
    {
      TypeId = typeId;
      TypeName = typeName;
    }

    public int TypeId { get; set; }
    public string TypeName { get; set; }
}


public class TransactionSimple : BaseViewModel
{
    public TransactionSimple(int transactionId, string description, int typeId, decimal amount)
    {
      TransactionId = transactionId;
      Description = description;
      TypeId = typeId;
      Amount = amount;
    }

    public int TransactionId { get; set; }
    public string Description { get; set; }
    private int _typeId;

    public int TypeId
    {
      get { return _typeId; }
      set
      {
        _typeId = value;
        OnPropertyChanged(nameof(TypeId));
      }
    }

    public decimal Amount { get; set; }
}

public class TransactionComplex : BaseViewModel
{
    public TransactionComplex(int transactionId, string description, int typeId, string typeName, decimal amount)
    {
      TransactionId = transactionId;
      Description = description;
      Type = new Type(typeId, typeName);
      Amount = amount;
    }

    public int TransactionId { get; set; }
    public string Description { get; set; }
    private Type _type;

    public Type Type
    {
      get { return _type; }
      set
      {
        if(_type != null) { MessageBox.Show($"Change to {value.TypeName}"); }
        _type = value;
        OnPropertyChanged(nameof(Type));
      }
    }

    public decimal Amount { get; set; }
}

And the ViewModel:

public sealed class MainWindowViewModel : BaseViewModel
{
    private ObservableCollection<TransactionSimple> _simples;
    private ObservableCollection<TransactionComplex> _complexes;


    public MainWindowViewModel()
    {
      FakeRepo();
    }

    private ReadOnlyCollection<Type> _types;

    public ReadOnlyCollection<Type> Types
    {
      get => (_types != null) ? _types : _types = new ReadOnlyCollection<Type>(new List<Type> { new Type(1, "Credit"), new Type(2, "Debit") });
    }


    public ObservableCollection<TransactionSimple> Simples
    {
      get { return _simples; }
      set
      {
        _simples = value;
        OnPropertyChanged(nameof(Simples));
      }
    }
    public ObservableCollection<TransactionComplex> Complexes
    {
      get { return _complexes; }
      set
      {
        _complexes = value;
        OnPropertyChanged(nameof(Complexes));
      }
    }

    private void FakeRepo()
    {
      var data = new List<TransactionComplex>
      {
        new TransactionComplex(1, "Got some money", 1, "Credit", 1000m),
        new TransactionComplex(2, "spent some money", 2, "Debit", 100m),
        new TransactionComplex(3, "spent some more money", 2, "Debit", 300m)
      };

      Complexes = new ObservableCollection<TransactionComplex>(data);
      Simples = new ObservableCollection<TransactionSimple>(data.Select(x => new TransactionSimple(x.TransactionId, x.Description, x.Type.TypeId, x.Amount)));
    }
}

UPDATED 2:24 PM PST USA: And finally the view(almost working):

<Window x:Class="ComboBoxInDataGridViewWPF.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:ComboBoxInDataGridViewWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <CollectionViewSource x:Key="Types" Source="{Binding Types}"/>
  </Window.Resources>
    <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Label Content="SimpleExample" />
    <DataGrid Grid.Row="1" ItemsSource="{Binding Simples}" AutoGenerateColumns="False">
      <DataGrid.Columns>
        <DataGridTextColumn Header="TransactionId" Binding="{Binding TransactionId}" />
        <DataGridTextColumn Header="Description" Binding="{Binding Description}" />
        <DataGridComboBoxColumn Header="Type" ItemsSource="{Binding Source={StaticResource Types}}" DisplayMemberPath="TypeName" SelectedValuePath="TypeId" SelectedValueBinding="{Binding Path=TypeId}" />
        <DataGridTextColumn Header="Amount" Binding="{Binding Amount}" />
      </DataGrid.Columns>
    </DataGrid>
    <Border Grid.Row="2" Height="50" Background="Black" />
    <Label Content="ComplexObjectExample" Grid.Row="3" />
    <DataGrid Grid.Row="4" ItemsSource="{Binding Complexes}" AutoGenerateColumns="False">
      <DataGrid.Columns>
        <DataGridTextColumn Header="TransactionId" Binding="{Binding TransactionId}" />
        <DataGridTextColumn Header="Description" Binding="{Binding Description}" />

        <!--This one works for the displays but not for the updates
        <DataGridTemplateColumn Header="Type">
          <DataGridTemplateColumn.CellEditingTemplate>
            <DataTemplate>
              <ComboBox ItemsSource="{Binding Source={StaticResource Types}}" DisplayMemberPath="TypeName"  SelectedItem="{Binding Type, Mode=TwoWay}" SelectedValue="{Binding Type.TypeId}" />
            </DataTemplate>
          </DataGridTemplateColumn.CellEditingTemplate>
          <DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
              <TextBlock Text="{Binding Type.TypeName}"/>
            </DataTemplate>
          </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>-->

        <!--This one works but the initial displays are wrong. This seems to be the closest to what I want-->
        <DataGridComboBoxColumn Header="Type" SelectedItemBinding="{Binding Type}"  >
          <DataGridComboBoxColumn.ElementStyle>
            <Style TargetType="ComboBox">
              <Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Types}"/>
              <Setter Property="DisplayMemberPath" Value="TypeName" />
              <Setter Property="SelectedItem" Value="{Binding Type}" />
              <Setter Property="IsReadOnly" Value="True"/>
            </Style>
          </DataGridComboBoxColumn.ElementStyle>
          <DataGridComboBoxColumn.EditingElementStyle>
            <Style TargetType="ComboBox">
              <Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Types}"/>
              <Setter Property="DisplayMemberPath" Value="TypeName" />
              <Setter Property="SelectedItem" Value="{Binding Type}" />
            </Style>
          </DataGridComboBoxColumn.EditingElementStyle>
        </DataGridComboBoxColumn>

        <!--This one does not work at all
        <DataGridTemplateColumn Header="Type">
          <DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
              <ComboBox ItemsSource="{Binding Path=DataContext.Types,
                                            RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"
                      DisplayMemberPath="TypeName" SelectedItem="{Binding Type}"/>
            </DataTemplate>
          </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>-->
        <DataGridTextColumn Header="Amount" Binding="{Binding Amount}" />
      </DataGrid.Columns>
    </DataGrid>
  </Grid>
</Window>

The problem is shown like this: enter image description here

I can obviously get the items bound to the ComboBox and I have seen by adding Observable Collections(not shown) and raising properties that the complex type is getting called. But it will not display no matter what I try. Trying the property of the property like Type.TypeName or such with different combinations doesn't work. Any ideas?


Solution

  • This ridiculous behaviour is well known. Because DataGridColumn lies not in the visual tree, the classic way using the DataGridComboBoxColumn to bind the items from parent like you tried is not working.

    Instead you could create DataGridTemplateColumn with a ComboBox inside. This should solve your problem nearly in the same way. If you want to bind the TypeId this code works:

    <DataGridTemplateColumn Header="Type">
        <DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
                <ComboBox ItemsSource="{Binding Path=DataContext.Types,
                                                RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"
                          DisplayMemberPath="TypeName"
                          SelectedValuePath="TypeId"
                          SelectedValue="{Binding Path=TypeId, UpdateSourceTrigger=PropertyChanged}"/>
            </DataTemplate>
        </DataGridTemplateColumn.CellTemplate>
    </DataGridTemplateColumn>
    

    Binding the whole Type could be done by changing the ComboBox to:

    <ComboBox ItemsSource="{Binding Path=DataContext.Types,
                                    RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"
              DisplayMemberPath="TypeName"
              SelectedItem="{Binding Path=Type, UpdateSourceTrigger=PropertyChanged}"/>
    

    Alternatively you can have a look at this question where other possible solutions are described.