27

I want to enable the user to edit some data in WPF DataGrid ( from the .net Framework 4.0). The "instruments" column should allow the user to select an available intrument from a static list or to write a free text. My DataGrid is binded to data using MVVM. I've tried many solutions I've found in internet but none of them work correctly. Here is my code:

<DataGrid Margin="0,6" ItemsSource="{Binding Path=Orders}" AutoGenerateColumns="False"  CanUserAddRows="False" CanUserDeleteRows="False" CanUserResizeRows="True">
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Instrument" MinWidth="140"                                      
 ItemsSource="{x:Static ViewModel.Instruments}" SelectedItemBinding="{Binding Path=SelectedInstrument}">
 <DataGridComboBoxColumn.EditingElementStyle>
   <Style TargetType="ComboBox">
     <Setter Property="IsEditable" Value="True"/>
   </Style>                  
 </DataGridComboBoxColumn.EditingElementStyle>                
</DataGridComboBoxColumn>   
</DataGrid.Columns>
</DataGrid>

The drop-down-list is shown correctly. The field can be edited with any text, but it sets a null to the SelectedInstrument after the drop-down is closed for the free text. It works only for the selected item. I've tried to change to SelectedValueBinding, but it doesn't help.

How to implement this requirements properly? Can someone post here a working sample?

Additional: Orders is ObservableCollection Order has Property like string Title, DateTime Ordered, string SelectedInstrument, Instruments is a string[]

Solutions: Following suggest as a workaround from bathineni works:

<DataGrid Margin="0,6" ItemsSource="{Binding Path=Orders}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserResizeRows="True">
 <DataGrid.Columns>
  <DataGridTemplateColumn Header="Instrument" MinWidth="140">
   <DataGridTemplateColumn.CellTemplate>
    <DataTemplate>
     <TextBlock Text="{Binding Path=SelectedInstrument, Mode=OneWay}"/>
    </DataTemplate>
   </DataGridTemplateColumn.CellTemplate>
   <DataGridTemplateColumn.CellEditingTemplate>
    <DataTemplate>
     <ComboBox IsEditable="True" Text="{Binding Path=SelectedInstrument}" 
      ItemsSource="{x:Static ViewModel.Instruments}"/>                   
    </DataTemplate>
   </DataGridTemplateColumn.CellEditingTemplate>
  </DataGridTemplateColumn>   
 </DataGrid.Columns>
</DataGrid>
Community
  • 1
  • 1
Alexander Zwitbaum
  • 4,776
  • 4
  • 48
  • 55

4 Answers4

19

this is happening because the free text which is enter is of type string and selected item what you have binded to the comboBox is of some complex type....

instead of using DataGridComboBoxColumn use DataGridTemplateColumn and you can bind Text property of the comboBox to some property which will hold the free text value after closing drop down list.

you can get better idea by looking at the following sample.

<DataGrid>
    <DataGrid.Columns>
        <DataGridTemplateColumn>
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <ComboBox IsEditable="True" 
                              Text="{Binding NewItem}" 
                              ItemsSource="{Binding Sourcelist.Files}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>
ASh
  • 34,632
  • 9
  • 60
  • 82
Bathineni
  • 3,436
  • 18
  • 25
  • Thank you, it works well if I add also – Alexander Zwitbaum Aug 01 '11 at 15:33
  • 7
    This is an awful user experience. You have to tab once into this column and then tab a 2nd time into the combobox. All other columns in the datagrid only require 1 tab, this one requires pressing tab twice. – Nick May 05 '15 at 14:12
  • Using DataGridTemplateColumn + ComboBox also makes pressing the "Tab" key to change between cells weird. It will stuck at the ComboBox column or continue to the next cell (right) from the last selected normal cell (left). – The Anh Nguyen Jul 30 '20 at 06:39
8

Try to use SelectedValue only but along with it use DisplayMemberPath and TextSearch.TextPath.

   <ComboBox IsEditable="True" DisplayMemberPath="MyDisplayProperty" SelectedValuePath="MyValueProperty" SelectedValue="{Binding MyViewModelValueProperty}" TextSearch.TextPath="MyDisplayProperty" />

For editable comboboxes we must synchronize what value the combo selects, what value the items display and what value we must search based on user input.

But If you are using a string collection to bind your combobox then you can try following...

  1. Add a new property in your ViewModel called InstrumentsView. This returns a new ListCollectionView.

    public static string ListCollectionView InstrumentsView
    {
            get
            {
                    return new ListCollectionView(Instruments);
            }
    }
    
  2. Change your DataGridComboBoxColumn XAML as below...

    <DataGridComboBoxColumn Header="Instrument" MinWidth="140"
                            ItemsSource="{x:Static ViewModel.InstrumentsView}">
            <DataGridComboBoxColumn.EditingElementStyle>
                    <Style TargetType="ComboBox">
                            <Setter Property="IsEditable" Value="True"/>
                            <Setter Property="IsSynchronizedWithCurrentItem" Value=True" />
                            <Setter Property="SelectedItem" Value="{Binding SelectedInstrument, Mode=OneWayToSource}" /> <!-- Assuming that SelectedInstrument is string  -->
                    </Style>
            </DataGridComboBoxColumn.EditingElementStyle>
    </DataGridComboBoxColumn>
    

Tell me if this works....

WPF-it
  • 19,625
  • 8
  • 55
  • 71
  • Are you mean to change DataGridComboBoxColumn.EditingElementStyle or to use DataGridTemplateColumn instead of the DataGridComboBoxColumn? And my object doesn't have any SubProperties like MyValueProperty, because it's a string. – Alexander Zwitbaum Aug 01 '11 at 14:32
  • Please see my edited reply above with new possible solution for string collection. – WPF-it Aug 01 '11 at 15:12
  • If SelectedInstrument {get { return InstrumentView.CurrentItem; } } how can I implement set to initialize initial view? CurrentItem is readonly. – Alexander Zwitbaum Aug 01 '11 at 15:30
  • Unfortunately your solution doesn't work. I get an empty drop-down and my previously SelectedInstrument is not shown in the closed cell. – Alexander Zwitbaum Aug 01 '11 at 15:57
5

You can create your own ComboBox column type by subclassing DataGridBoundColumn. Compared to bathineni's solution of subclassing DataGridTemplateColumn the below solution has the benefit of better user experience (no double-tabbing) and you have more options to tune the column to your specific needs.

public class DataGridComboBoxColumn : DataGridBoundColumn {
    public Binding ItemsSourceBinding { get; set; }

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) {
        var textBox = new TextBlock();
        BindingOperations.SetBinding(textBox, TextBlock.TextProperty, Binding);
        return textBox;
    }

    protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) {
        var comboBox = new ComboBox { IsEditable = true };
        BindingOperations.SetBinding(comboBox, ComboBox.TextProperty, Binding);
        BindingOperations.SetBinding(comboBox, ComboBox.ItemsSourceProperty, ItemsSourceBinding);
        return comboBox;
    }

    protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs) {
        var comboBox = editingElement as ComboBox;
        if (comboBox == null) return null;

        comboBox.Focus(); // This solves the double-tabbing problem that Nick mentioned.
        return comboBox.Text;
    }
}

You can then use the component for example like this.

<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MyItems}">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
        <local:DataGridComboBoxColumn Header="Thingy" Binding="{Binding Thingy}"
            ItemsSourceBinding="{Binding
                RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}},
                Path=Thingies}"/>
    </DataGrid.Columns>
</DataGrid>

I got this solution by following this answer to a similar question.

Community
  • 1
  • 1
vvnurmi
  • 448
  • 6
  • 10
1

Maybe it'll still be useful to someone. This solution allows to add new entered values to selection list and has no side effects while editing.

XAML:

<DataGridTemplateColumn Header="MyHeader" Width="Auto">
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <ComboBox IsEditable="True"
                Text="{Binding MyTextProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                DisplayMemberPath="MyTextProperty"
                SelectedValuePath="MyTextProperty" 
                ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=DataContext.SelectionList}"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

ViewModel:

public class MyViewModel 
{
    public class MyItem : INotifyPropertyChanged {
        private string myTextProperty;
        public string MyTextProperty {
            get { return myTextProperty; }
            set { myTextProperty = value;
                OnPropertyChanged("MyTextProperty"); }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName]string prop = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
        }
    }
    public ObservableCollection<MyItem> MyItems { get; set; }
    public object SelectionList { get; set; }
}

CodeBehinde:

MyWindow.DataContext = MyViewModelInstance;
MyDataGrid.ItemsSource = MyItems;

// Before DataGrid loading and each time after new MyProperty value adding, you must execute:
MyViewModelInstance.SelectionList = MyViewModelInstance.MyItems.OrderBy(p => p.MyTextProperty).GroupBy(p => p.MyTextProperty).ToList();