views:

62

answers:

2

It looks WFP DataGridComboBoxColumn is using a single ItemsSource for all cells in this column. I have a case where the ComboBox items are dependent on the other cell in the same row. I managed to populate the ItemsSource in PreparingCellForEdit event. However, it doesn't work as desired. Initially, all cells in this column is empty. Once I populate the ItemsSource for this column's ComboBox, all related cells (with the same items source) are showing values. However, if I click another type of cell (a different items source is populated), all values disappear and the new type cells show values. You can only use one set of Items Source for a column? I can't believe it is true. Did I miss anything? Any workaround?

A: 

You probably can't do it reliably. The grid may reuse the combo box or randomly create/destroy it.

By chance I just happen to be working on a screen that does just that. Given these...

  • Each row in the grid is bound to an object of type Trade.
  • Each Trade has a State property
  • Each Trade has a TerritoryCanidates property
  • Changing the State property will cause the TerritoryCanidates property to change

This gives me the ability to bind the ItemsSource to the TerritoryCanidates property. Which in turn the DataGrid will honor under all circumstances.


<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
    <DataGrid Name="Zoom" AutoGenerateColumns="False">
        <DataGrid.Columns>
            <DataGridTemplateColumn Header="State">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding State}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate>
                        <ComboBox SelectedItem="{Binding State}" ItemsSource="{Binding StateCanidates}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>

            <DataGridTemplateColumn Header="Territory">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Territory}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate>
                        <ComboBox SelectedItem="{Binding Territory}" ItemsSource="{Binding TerritoryCanidates}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>

        </DataGrid.Columns>

    </DataGrid>
</Grid>
</Window>


Imports System.ComponentModel

Class MainWindow
Sub New()

    ' This call is required by the designer.
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.
    Dim x As New List(Of Model)
    x.Add(New Model)
    x.Add(New Model)
    x.Add(New Model)

    Zoom.ItemsSource = x
End Sub
End Class

Class Model
Implements INotifyPropertyChanged

Public ReadOnly Property StateCanidates As List(Of String)
    Get
        Return New List(Of String) From {"CA", "TX", "NY"}
    End Get
End Property

Public ReadOnly Property TerritoryCanidates As List(Of String)
    Get
        If State = "" Then Return Nothing
        Return New List(Of String) From {State & "1", State & "2"}
    End Get
End Property

Private m_State As String
Public Property State() As String
    Get
        Return m_State
    End Get
    Set(ByVal value As String)
        m_State = value
        OnPropertyChanged("State")
        OnPropertyChanged("TerritoryCanidates")
    End Set
End Property

Private m_Territory As String
Public Property Territory() As String
    Get
        Return m_Territory
    End Get
    Set(ByVal value As String)
        m_Territory = value
        OnPropertyChanged("Territory")
    End Set
End Property




Public Sub OnPropertyChanged(ByVal propertyName As String)
    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub

Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
End Class
Jonathan Allen
@Jonathan: I'm confused...Does it work for you? I just tried it and couldn't make it work. What I tried is like this: I made a new collection for the ItemsSource to bind. This collection will be recreated when another property changes. I don't see anything in combobox dropdown.
miliu
Odd, it seems like the built-in combo box column isn't working like I expected. I re-did my example using template columns like my real program uses.
Jonathan Allen
P.S. I now officially hate the DataGrid for editing data. I am instead going to just use an ItemsControl.
Jonathan Allen
@Jonathan: Inspired by your example, I made it work now. I'll post my code as an answer due to the space limit here. Thank you very much for your help!
miliu
A: 

Thanks to Jonathan's example, I resolved my problem as follows. I modifiedy Jonathan's code to highlight my solution. I removed the Territory property from his example because I don't need it for my problem.

There are two columns. First column is State. Second column is StateCandidate. State column is bind to a States list, and StateCandidate column is bind to a StateCandidates list. The key point is that the StateCandidates list is recreated when State is changed. So, there could be a different list of StateCandidates in each row (based on the selected State).

MainWindow.xaml

<Window x:Class="WpfTest1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid Name="Zoom" AutoGenerateColumns="False" Background="DarkGray" RowHeaderWidth="50" HeadersVisibility="All">
            <DataGrid.Columns>
                <DataGridTemplateColumn x:Name="colState" Header="State" Width="120">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding State}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <ComboBox SelectedItem="{Binding State}" ItemsSource="{Binding States}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn x:Name="colStateCandiate" Header="State Candidate" Width="200">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding StateCandidate}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <ComboBox SelectedItem="{Binding StateCandidate}" ItemsSource="{Binding StateCandidates}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;

namespace WpfTest1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            List<Model> list = new List<Model>();
            list.Add(new Model() { State = "TX", StateCandidate = "TX2" });
            list.Add(new Model() { State = "CA" });
            list.Add(new Model() { State = "NY", StateCandidate = "NY1" });
            list.Add(new Model() { State = "TX" });
            list.Add(new Model() { State = "AK" });
            list.Add(new Model() { State = "MN" });

            Zoom.ItemsSource = list;
            Zoom.PreparingCellForEdit += new EventHandler<DataGridPreparingCellForEditEventArgs>(Zoom_PreparingCellForEdit);
        }

        void Zoom_PreparingCellForEdit(object sender, DataGridPreparingCellForEditEventArgs e)
        {
            if (e.Column == colStateCandiate)
            {                
                DataGridCell cell = e.Column.GetCellContent(e.Row).Parent as DataGridCell;
                cell.IsEnabled = (e.Row.Item as Model).StateCandidates != null;
            }
        }
    }
    public class Model : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string _state;
        private List<string> _states = new List<string>() { "CA", "TX", "NY", "IL", "MN", "AK" };
        private string _stateCandidate;
        private List<string> _stateCandidates;

        public string State
        {
            get { return _state; }
            set
            {
                if (_state != value)
                {
                    _state = value;
                    _stateCandidate = null;
                    if (_state == "CA" || _state == "TX" || _state == "NY")
                        _stateCandidates = new List<string> { _state + "1", _state + "2" };
                    else
                        _stateCandidates = null;
                    OnPropertyChanged("State");
                }
            }
        }
        public List<string> States
        {
            get { return _states; }
        }
        public string StateCandidate
        {
            get { return _stateCandidate; }
            set 
            {
                if (_stateCandidate != value)
                {
                    _stateCandidate = value;
                    OnPropertyChanged("StateCandidate");
                }
            }
        }
        public List<string> StateCandidates
        {
            get { return _stateCandidates; }
        }
        public void OnPropertyChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}

Note that, when State is changed, it won't update StateCandidates list until a different row is selected, which is a separated issue I'll be fighting. Do anybody know how I can force commit?

Thanks to Jonathan again for his inspiration. I'll keep looking for a better solution.

miliu
You are going down the exact same road to nowhere I was on. But since you insist, here's the code: http://www.scottlogic.co.uk/blog/colin/tag/ieditableobject/
Jonathan Allen
@Jonathan: Thank you very much for the link. However, I couldn't make that work for this sample, maybe because I don't use DataTable. While strugling with that, I found a comment in that blog pointing to http://codefluff.blogspot.com/2010/05/commiting-bound-cell-changes.html. This solution works great.
miliu
My appologies, I posted the wrong link. I'm not using a DataTable either, your link is actually the one I ended up using.
Jonathan Allen