The basic issue is that I had a DataGrid with one column for a description and a second column for data. I wanted to the second column to be editable or have a drop down combo box for the user to select an item from a list. I quickly learned that though it is possible to have a built-in DataGridComboColumn, you can't necessarily turn those off, or have a default when there aren't multiple items to chose from. I also wanted two-way binding with these cells. There are some clever attempts out there, and perhaps some of those may work for you with what you actually intend. I came up with my own method. To summarize:
1. Create the columns, in my case just two, in the Xaml definition of the DataGrid, the first one being a TextColumn, the second a TemplateColumn. Any extra columns that will have mixed formatting should be TemplateColumns.
2. Got my list of cells which only exists after the DataGrid has been rendered.
3. Create the two templates *in code* (one ComboBox for a drop-down selection cell, one TextBox for a 'simple' data entry cell).
4. Create a *map* of templates to correspond to the respective cell. This in practice was just a List<DataTemplate> of DataTemplates.
5. Apply the map of templates onto the rendered cells.
FindVisualChild has got to be one of the most popular WPF methods out there that is user-generated. I wouldn't be surprised if some kind of equivalent method makes its way into a future version of .NET. A simple Google search will turn up this method but I'll post yet another copy here for transparency's sake:
Code:
/// <summary> /// Look for a child element /// </summary> /// <typeparam name="childItem">The element being searched for</param> /// <param name="obj">The visual tree container</param> /// <returns>The element being searched for, or null if it is not found.</param> private childItem FindVisualChild<childItem>(DependencyObject obj) where childItem : DependencyObject { if (obj == null) { return null; } int childCount = VisualTreeHelper.GetChildrenCount(obj); for (int] i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++) { DependencyObject child = VisualTreeHelper.GetChild(obj, i); if (child != null && child is childItem) { return (childItem)child; } else { childItem childOfChild = FindVisualChild<childItem>(child); if (childOfChild != null) { return childOfChild; } } } return null; }
private List<DataGridCell> dataCellList = new List<DataGridCell>();
To get my cells one by one using this method:
Code:
/// <summary> /// Get the cell of the datagrid. /// </summary> /// <param name="dataGrid">The data grid in question</param> /// <param name="cellInfo">The cell information for a row of that datagrid</param> /// <param name="cellIndex">The row index of the cell to find if we decide to use this to get the cell</param> /// <returns>The cell or null</param> private DataGridCell TryToFindGridCell(DataGrid dataGrid, DataGridCellInfo cellInfo, int cellIndex = -1) { DataGridRow row; DataGridCell result = null; if (cellIndex < 0) { row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromItem(cellInfo.Item); } else { row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(cellIndex); } if (row != null) { int columnIndex = dataGrid.Columns.IndexOf(cellInfo.Column); if (columnIndex > -1) { DataGridCellsPresenter presenter = this.FindVisualChild<DataGridCellsPresenter>(row); result = presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex) as DataGridCell; } } return result; }
Code:
/// <summary> /// This method sorts through the rendered grid and gets all of the cells. /// Then, depending on whether there are multiple selection in the combobox /// defined in the template, the cell is either a combo box or a textbox. /// </summary> private void PrivateDataGrid_Initialized() { DataGrid dataGrid = this.dataGrid1;int rowNumber = 0;for (int k = 0; k < dataGrid.Columns.Count; k++) { for (int i = 0; i < dataGrid.Items.Count; i++) { if (this.dataCellList.Count < (dataGrid.Columns.Count * dataGrid.Items.Count)) { int j = i + (dataGrid.Items.Count * k); this.dataCellInfoList.Add(new DataGridCellInfo(dataGrid.Items[i], dataGrid.Columns[k])); this.dataCellList.Add(this.TryToFindGridCell(dataGrid, this.dataCellInfoList[j], i)); } } } //// Just clear out the list and let this handler try again until we get non-null cells. if (this.dataCellList[0] == null) { this.dataCellList.Clear(); } else { //// After we start getting valid cells, assign the correct template. this.columnIndex = 1; for (int k = this.columnIndex * dataGrid.Items.Count; k < (this.columnIndex + 1) * (this.dataCellList.Count - this.dataGrid.Items.Count); k++) { this.dataCellList[k].ContentTemplate = this.dataTemplateList[rowNumber]; rowNumber++; } }} }
So let's get that list of templates put together. I use a collection (an ObservableCollection in fact) to hold my data lists. If a particular list has more than one string in it, I put a ComboBox template in the DataTemplate list, otherwise if there is just one string in the list, I put a simple TextBox template in the DataTemplate list. I call this method after I have done my data binding for my grid. The code for my list of templates:
Code:
/// <summary>/// Create a list of DataTemplates to display either a combo box/// or a plain text box./// </summary>private void CreateDataTemplates(){//// If the list has multiple entries,//// use a combo box to diplay the options.//// Otherwise, just use a plain text box.for (int i = 0; i < this.OptionList.Count; i++){if (this.OptionList[i].Count > 1){string bindingString = "OptionList" + "[" + i.ToString() + "]";Binding comboBinding = new Binding(bindingString);comboBinding.Mode = BindingMode.TwoWay;comboBinding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor) { AncestorType = typeof(Window) };#region ComboBox TemplateDataTemplate dataCellComboTemplate = new DataTemplate();dataCellComboTemplate.RegisterName("comboBoxCellTemplate", dataCellComboTemplate);////Set up the stack panelFrameworkElementFactory spFactory = new FrameworkElementFactory(typeof(StackPanel));spFactory.Name = "stackPanelFactory";FrameworkElementFactory cbFactory = new FrameworkElementFactory(typeof(ComboBox));cbFactory.Name = "comboBoxFactory";cbFactory.SetValue(ComboBox.NameProperty, "dataComboBox");cbFactory.SetValue(ComboBox.HeightProperty, Double.NaN);cbFactory.SetValue(ComboBox.SelectedIndexProperty, 0);cbFactory.AddHandler(ComboBox.SelectedEvent, new RoutedEventHandler(this.ComboBox_SelectedItem));cbFactory.SetBinding(ComboBox.DataContextProperty, new Binding());cbFactory.SetBinding(ComboBox.ItemsSourceProperty, comboBinding);spFactory.AppendChild(cbFactory);dataCellComboTemplate.VisualTree = spFactory;#endregionthis.dataTemplateList.Add(dataCellComboTemplate);}else{DataTemplate dataCellTextTemplate = new DataTemplate();dataCellTextTemplate.RegisterName("textBoxCellTemplate", dataCellTextTemplate);string bindingString = "OptionList" + "[" + i.ToString() + "]" + "[" + "0" + "]";Binding textBinding = new Binding(bindingString);textBinding.Mode = BindingMode.TwoWay;textBinding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor) { AncestorType = typeof(Window) };#region Plain TextBox TemplateFrameworkElementFactory plainSpFactory = new FrameworkElementFactory(typeof(StackPanel));plainSpFactory.Name = "plainStackPanelFactory";FrameworkElementFactory plainTextBoxFactory = new FrameworkElementFactory(typeof(TextBox));plainTextBoxFactory.Name = "cellTextBlockFactory";plainTextBoxFactory.SetValue(TextBox.NameProperty, "dataTextBlock");plainTextBoxFactory.SetValue(TextBox.HeightProperty, Double.Parse("25"));plainTextBoxFactory.SetValue(TextBox.WidthProperty, Double.NaN);plainTextBoxFactory.SetBinding(TextBox.DataContextProperty, new Binding());plainTextBoxFactory.SetBinding(TextBox.TextProperty, textBinding);plainSpFactory.AppendChild(plainTextBoxFactory);dataCellTextTemplate.VisualTree = plainSpFactory;#endregionthis.dataTemplateList.Add(dataCellTextTemplate);}}}
One note about separation of concerns: You could go ahead and generate those columns in code as well and apply the Itemsource appropriately. This would have the display only show the base elements while your code handles how they are displayed and what is displayed. Your business logic and data are naturally separated off onto there own concerns. I don't do this View-View separation here as I'm not so concerned about this level of separation for my particular case. But it is something to keep in mind, especially if things get a lot more complicated.
Another thing: In my research for this solution and help with other issues regarding Xaml elements, I've come across the general notion that it is not a good idea to go overwriting templates. I don't subscribe to this idea and I don't think Microsoft does either. Just look at the interfaces they make available and methods they've developed in order to make it so easy for us to do just exactly that.