Search code examples
c#wpfdatagriddatagridcomboboxcolumn

IValueConverter error for DataGridComboBoxColumn


So I have a DataGridComboBoxColumn ColCID whose value depends on another cell of the row ColSID (see below code). I tried to implement it with an IValueConverter but I get this error:

Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:(no path); DataItem=null; target element is 'DataGridComboBoxColumn' (HashCode=18018639); target property is 'ItemsSource' (type 'IEnumerable')

XAML:

<DataGridComboBoxColumn x:Name="ColSID" Header="Guild"
                        SelectedValueBinding="{Binding SID, Mode=TwoWay}"
                        SelectedValuePath="SID"
                        DisplayMemberPath="Name" />
<DataGridComboBoxColumn x:Name="ColCID" Header="Channel"
                        ItemsSource="{ Binding ElementName=ColSID, Converter={StaticResource ChannelConverter} }"
                        SelectedValueBinding="{Binding CID, Mode=TwoWay}"
                        SelectedValuePath="CID"
                        DisplayMemberPath="Name" />

Converter:

public class ChannelConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Guild guild = SenderView.Guilds.Find(g => g.SID == value.ToString());
        if (guild != null) return guild.Channels;
        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
...
public class Guild
{
    public class Channel
    {
        public string CID { get; set; }
        public string Name { get; set; }
    }
    public string SID { get; set; }
    public string Name { get; set; }
    public List<Channel> Channels { get; set; }
}

Solution

  • Looks to me like for each row in your grid, you want the user to be able to select a Guild, and a Channel for the Guild. Each Guild has its own collection of Channel, so we're going to select from that collection. I'm going to bind the Channel and Guild objects, not just their IDs. We could do it the other way if you prefer, but I find it easier to let the ComboBox look up the object, instead of the rest of my code having to do it. The SID or CID is right there on the selected object when I want it.

    Here's how you can do that. You can't conventionally bind anything to DataGridComboBoxColumn.ItemsSource in your XAML. Any solution involving that property is going to be a little it exotic at best, but it's entirely unnecessary: You can bind the ComboBox's ItemsSource via ElementStyle and EditingElementStyle. SelectedValuePath and SelectedValueBinding do work, but I didn't use them.

    I wrote a quick stand-in for the parent viewmodel, which owns the collections of Guilds that the user can select from. Note that I also moved your Channel class out of Guild. The only reason I did that was so I could have a Channel property on the Guild class for the selected channel. If you'd rather keep Channel where you had it, simply rename Guild's Channel property to SelectedChannel or something like that, and alter the bindings in the XAML accordingly.

    The "framework mentor" nonsense is because the columns aren't child controls in the visual tree. They're an instruction to the DataGrid to create a column header, and also a cell in each row. Those headers and cells, and so also their templated content, are in the visual tree. SelectedItemBinding isn't a binding on the column; it's a binding that the column creation code will set on the SelectedItem property of a ComboBox that it will eventually create in the cell content. But the ItemsSource property is just a property of the column itself. You could bind it with a binding proxy, and people do, but binding proxies are the coward's way out.

    XAML

    <DataGrid
        ItemsSource="{Binding Selections}"
        AutoGenerateColumns="False"
        Grid.Row="1"
        >
        <DataGrid.Resources>
            <Style TargetType="ComboBox" x:Key="GuildComboStyle">
                <Setter 
                    Property="ItemsSource" 
                    Value="{Binding DataContext.Guilds, RelativeSource={RelativeSource AncestorType=DataGrid}}" 
                    />
            </Style>
            <Style 
                TargetType="ComboBox" 
                x:Key="ChannelComboStyle"
                >
                <!-- Our DataContext here is a GuildSelection object, so we look at its Guild
                property for a collection of Channels to use.
                -->
                <Setter  Property="ItemsSource" Value="{Binding Guild.Channels}" />
                <!-- If there's no selected Guild, prompt the user to select one. -->
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Guild}" Value="{x:Null}">
                        <Setter Property="IsEnabled" Value="False" />
                        <Setter Property="IsEditable" Value="True" />
                        <Setter Property="Text" Value="Please select a Guild" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </DataGrid.Resources>
        <DataGrid.Columns>
            <DataGridComboBoxColumn 
                Header="Guild"
                SelectedItemBinding="{Binding Guild, UpdateSourceTrigger=PropertyChanged}"
                DisplayMemberPath="Name" 
                ElementStyle="{StaticResource GuildComboStyle}"
                EditingElementStyle="{StaticResource GuildComboStyle}"
                Width="200"
                />
            <DataGridComboBoxColumn 
                Header="Channel"
                SelectedItemBinding="{Binding Guild.Channel, UpdateSourceTrigger=PropertyChanged}"
                DisplayMemberPath="Name" 
                ElementStyle="{StaticResource ChannelComboStyle}"
                EditingElementStyle="{StaticResource ChannelComboStyle}"
                Width="200"
                />
    
            <DataGridTextColumn Binding="{Binding Guild.SID}" Header="Guild SID" />
            <DataGridTextColumn Binding="{Binding Guild.Name}" Header="Guild Name" />
            <DataGridTextColumn Binding="{Binding Guild.Channel.CID}" Header="Channel CID" />
            <DataGridTextColumn Binding="{Binding Guild.Channel.Name}" Header="Channel Name" />
        </DataGrid.Columns>
    </DataGrid>
    

    C#

    public class MainViewModel
    {
        public ObservableCollection<GuildSelection> Selections { get; set; }
        public ObservableCollection<Guild> Guilds { get; set; }
    }
    
    public class GuildSelection : ViewModelBase
    {
        #region Guild Property
        private Guild _guild = null;
        public Guild Guild
        {
            get { return _guild; }
            set
            {
                if (value != _guild)
                {
                    _guild = value;
                    OnPropertyChanged();
                }
            }
        }
        #endregion Guild Property
    }
    
    public class Channel
    {
        public string CID { get; set; }
        public string Name { get; set; }
    }
    
    public class Guild : ViewModelBase
    {
        public string SID { get; set; }
        public string Name { get; set; }
        public List<Channel> Channels { get; set; }
    
        #region Channel Property
        private Channel _channel = null;
        public Channel Channel
        {
            get { return _channel; }
            set
            {
                if (value != _channel)
                {
                    _channel = value;
                    OnPropertyChanged();
                }
            }
        }
        #endregion Channel Property
    }
    
    #region ViewModelBase Class
    public class ViewModelBase : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected virtual void OnPropertyChanged(
            [System.Runtime.CompilerServices.CallerMemberName] string propName = null) 
                => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        #endregion INotifyPropertyChanged
    }
    #endregion ViewModelBase Class