none
WPF DataGrid and binding to matrix of objects using UserControl per cell RRS feed

  • Question

  • I’ve got a collection of objects that represents some kind of my data. This collection is binded to ListBox and I have a DataTemplate containing my UserControl with binded properties to display data the way I want.

    This works pretty good.

    I want to make an upgrade and display this data in matrix with headers for columns and rows. Each object has knowledge of column and row that it belongs to. With this knowledge I’m able to create any 2D representation of objects (2D array, DataTable,…). I’m able to create columns and rows in any needed representation.

    I have tried to use DataGrid. I’ve created columns with headers (I have basically no clue, how many rows and columns I’ll have to show) for DataGrid and DataTable representing my data and binded it to DataGrid. I’ve used DataTemplateColumn with my UserControl to display my object. DataGrid displayed correctly amount of columns and rows, but the UserControl for object was not able to display anything. Every UserControl was given DataRowView object instead of my object and as so, I’m not able to represent data correctly.

    Is there a way, how can this be solved? How can I bind object of one cell to my UserControl in DataGrid?
    Monday, May 10, 2010 12:56 PM

Answers

  • Hi all,

    thanks for interest...

    After a little dissapoitment I have finally solved the problem. The problem was binding. There are two ways how data bind correctly:

    1. public partial class MainWindow : Window {
      
       public MainWindow(){
       InitializeComponent();
      
       MainViewModel vm = new MainViewModel();
       this.DataContext = vm;
      
       for (int i = 0; i < 10; i++){
       DataGridTemplateColumn col = new DataGridTemplateColumn();
       col.Header = i.ToString();
      
       FrameworkElementFactory fef = new FrameworkElementFactory(typeof(ContentPresenter));
       Binding binding = new Binding();  
       fef.SetBinding(ContentPresenter.ContentProperty, binding);
       fef.SetValue(ContentPresenter.ContentTemplateProperty, Resources["CellTemplate"] as DataTemplate);
      
       binding.Path = new PropertyPath("Values[" + i + "]");
       DataTemplate dataTemplate = new DataTemplate();
       dataTemplate.VisualTree = fef;
       col.CellTemplate = dataTemplate;
      
       dataGrid.Columns.Add(col);
       }
       }
      }
    2. public partial class MainWindow : Window {
      
       public MainWindow(){
       InitializeComponent();
      
       MainViewModel vm = new MainViewModel();
       this.DataContext = vm;
      
       for (int i = 0; i < 10; i++){
       DataGridTemplateColumn col = new DataGridTemplateColumn();
       col.Header = i.ToString();
      
       string dtString = @"<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""> <ContentPresenter ContentTemplate=""{DynamicResource CellTemplate}"" Content=""{Binding Values["+i+@"]}""/> </DataTemplate>";
      
       StringReader stringReader = new StringReader(dtString);
       XmlReader xmlReader = XmlReader.Create(stringReader);
       DataTemplate dataTemplate = (DataTemplate)System.Windows.Markup.XamlReader.Load(xmlReader);
      
       col.CellTemplate = dataTemplate;
      
       dataGrid.Columns.Add(col);
       }
       }
      }

    CellTemplate is template I'm looking for in code. It has to be implemented. For example like this:

    <DataTemplate x:Key="CellTemplate">
       <StackPanel>
         <Button>pico</Button>
         <TextBlock Text="{Binding}" />
       </StackPanel>
    </DataTemplate>

    And my view model looks like this:

     

     public class MainViewModel {
    
     private Row[] matrix;
     public Row[] Matrix { get { return matrix; } }
    
     public MainViewModel()
     {
      matrix = new Row[10];
      for (int i = 0; i < 10; i++)
      {
      matrix[i] = new Row("Row"+i.ToString());
      }
     }
     }
    
     public class Row
     {
     private string name;
     private string[] values;
    
     public string Name { get { return name; } }
     public string[] Values { get { return values; } }
    
     public Row(string name){
      this.name = name;
      values = new string[10];
      for (int i = 0; i < 10; i++){
      values[i] = "Col"+i.ToString();
      }
     }
     }

     

    It's just a project for develop, so ... ;-) ...

     

    • Marked as answer by kovman Friday, May 14, 2010 11:47 AM
    • Edited by kovman Friday, May 14, 2010 11:49 AM Added CellTemplate
    Friday, May 14, 2010 11:45 AM

All replies

  • Could you post  your code?
    Best Regards,
    Tom

    If you have found this post helpful, please click the Vote as Helpful link (the green triangle and number on the top-left).
    If this post answers your question, click the Mark As Answered link below. It helps others who experience the same issue in future to find the solution.
    Monday, May 10, 2010 12:58 PM
  • I'll try to give you as much source as possible. I was working on, and I have tried some other approaches, so the code developed.

    When I tried to used DataTable I created it in code behind of the window when the base collection changed and binded it to ItemsSource of DataGrid. As well as this I have created columns that were using this template:

    <DataTemplate x:Key="DataMatrixItemTemplate">
     <wpf:StatBox NumberFormating="{Binding Path=NumberFormating}"
     Value="{Binding Path=LastEntry}"
     LastTimeUpdate="{Binding Path=LastTimeUpdate}"
     TextForeground ="{Binding Path=TextForeground}"
     ShowTime="{Binding Path=ShowTime}"
     MinTreasholdLimit="{Binding Path=MinThreshold }"
     MaxTreasholdLimit="{Binding Path=MaxThreshold }">
     </wpf:StatBox>
    </DataTemplate>
    

    I tried to debug it and there was created as many StatBoxes as were cells. I also added another Dependency property to StatBox, where I binded just {Binding}. Therefor I can say, that binded property was DataRowView instead of my object representing data.

     

    Then I tried to modified it and I have created some kind of my representation of matrix. I have used the same template as above and this structure:

    public class DataMatrix : IEnumerable{
     public List<MatrixColumn> Columns { get; set; }
     public List<MatrixRow> Rows { get; set; }
    
     IEnumerator IEnumerable.GetEnumerator() { return new GenericEnumerator(Rows.ToArray());}  
    }
    
    public class MatrixColumn : INamedHeaders{
     private INamedHeaders headers;
     public MatrixColumn(INamedHeaders init){ headers = init; }
    
     public string RowName{ get { return headers.RowName; }}
     public string ColumnName { get { return headers.ColumnName; }}
     public string RowTitle { get { return headers.RowTitle; }}
     public string ColumnTitle { get { return headers.ColumnTitle; }}
    }
    
    public class MatrixRow : IEnumerable{
     public List<INamedHeaders> Items { get; set; }
    
     public MatrixRow() { Items = new List<INamedHeaders>(); }
    
     IEnumerator IEnumerable.GetEnumerator() { return new GenericEnumerator(Items.ToArray()); }
    }
    
    class GenericEnumerator : IEnumerator {
     private readonly object[] list;
     private int position = -1;
    
     public GenericEnumerator(object[] list) { this.list = list; }
    
     public bool MoveNext(){
     position++;
     return (position < list.Length);
     }
    
     public void Reset(){position = -1; }
    
     public object Current{
     get{
     try { return list[position]; }
     catch (IndexOutOfRangeException) { throw new InvalidOperationException(); }
     }}}
    
    public interface INamedHeaders{
     string RowName { get; }
     string ColumnName { get; }
     string RowTitle { get; }
     string ColumnTitle { get; }
    }
    

     

    WPF Window code behind generated the structure the very same way as before, but DataMatrix was result. To create the columns I have created the AttachedProperty and used Callback method like this:

     

    private static void OnMatrixSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){
     DataGrid dgv = d as DataGrid;
     if (dgv != null){
     dgv.LoadingRow += dgv_LoadingRow;
     dgv.Loaded += dgv_Loaded;
     DataMatrix dataMatrix = e.NewValue as DataMatrix;
    
     dgv.ItemsSource = dataMatrix;
     dgv.Columns.Clear();
     if (dataMatrix != null) {
     DataTemplate dataTemplate = (dgv.Resources["DataMatrixItemTemplate"] as DataTemplate);
     foreach (var col in dataMatrix.Columns){
      var newColumn = new DataGridTemplateColumn{
      Header = col.ColumnTitle,
      CellTemplate = dataTemplate};
      dgv.Columns.Add(newColumn);
    }}}
    

     

    I have tried this approach, because as I could understand DataGrid is able to go through IEnumerable elements and I wanted to debug it and see if it’s ok.

    It created everything correctly, but only the element at position 0 was displayed in every column.

    This approach was based on: http://www.codeproject.com/KB/WPF/WPF_DynamicListView.aspx I have just upgraded it for DataGrid (I tried to upgrade it :-/ ).
    • Edited by kovman Monday, May 10, 2010 2:12 PM C&P errors
    Monday, May 10, 2010 2:11 PM
  • Hi Kovman,

    Could you please reproduce the problem in a simple project and send it to me? My email address is v-lliu at microsoft dot com.

    Thanks,
    Linda Liu


    Please remember to mark the replies as answers if they help and unmark them if they provide no help.
    Welcome to the All-In-One Code Framework! If you have any feedback, please tell us.
    Friday, May 14, 2010 8:30 AM
  • Hi all,

    thanks for interest...

    After a little dissapoitment I have finally solved the problem. The problem was binding. There are two ways how data bind correctly:

    1. public partial class MainWindow : Window {
      
       public MainWindow(){
       InitializeComponent();
      
       MainViewModel vm = new MainViewModel();
       this.DataContext = vm;
      
       for (int i = 0; i < 10; i++){
       DataGridTemplateColumn col = new DataGridTemplateColumn();
       col.Header = i.ToString();
      
       FrameworkElementFactory fef = new FrameworkElementFactory(typeof(ContentPresenter));
       Binding binding = new Binding();  
       fef.SetBinding(ContentPresenter.ContentProperty, binding);
       fef.SetValue(ContentPresenter.ContentTemplateProperty, Resources["CellTemplate"] as DataTemplate);
      
       binding.Path = new PropertyPath("Values[" + i + "]");
       DataTemplate dataTemplate = new DataTemplate();
       dataTemplate.VisualTree = fef;
       col.CellTemplate = dataTemplate;
      
       dataGrid.Columns.Add(col);
       }
       }
      }
    2. public partial class MainWindow : Window {
      
       public MainWindow(){
       InitializeComponent();
      
       MainViewModel vm = new MainViewModel();
       this.DataContext = vm;
      
       for (int i = 0; i < 10; i++){
       DataGridTemplateColumn col = new DataGridTemplateColumn();
       col.Header = i.ToString();
      
       string dtString = @"<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""> <ContentPresenter ContentTemplate=""{DynamicResource CellTemplate}"" Content=""{Binding Values["+i+@"]}""/> </DataTemplate>";
      
       StringReader stringReader = new StringReader(dtString);
       XmlReader xmlReader = XmlReader.Create(stringReader);
       DataTemplate dataTemplate = (DataTemplate)System.Windows.Markup.XamlReader.Load(xmlReader);
      
       col.CellTemplate = dataTemplate;
      
       dataGrid.Columns.Add(col);
       }
       }
      }

    CellTemplate is template I'm looking for in code. It has to be implemented. For example like this:

    <DataTemplate x:Key="CellTemplate">
       <StackPanel>
         <Button>pico</Button>
         <TextBlock Text="{Binding}" />
       </StackPanel>
    </DataTemplate>

    And my view model looks like this:

     

     public class MainViewModel {
    
     private Row[] matrix;
     public Row[] Matrix { get { return matrix; } }
    
     public MainViewModel()
     {
      matrix = new Row[10];
      for (int i = 0; i < 10; i++)
      {
      matrix[i] = new Row("Row"+i.ToString());
      }
     }
     }
    
     public class Row
     {
     private string name;
     private string[] values;
    
     public string Name { get { return name; } }
     public string[] Values { get { return values; } }
    
     public Row(string name){
      this.name = name;
      values = new string[10];
      for (int i = 0; i < 10; i++){
      values[i] = "Col"+i.ToString();
      }
     }
     }

     

    It's just a project for develop, so ... ;-) ...

     

    • Marked as answer by kovman Friday, May 14, 2010 11:47 AM
    • Edited by kovman Friday, May 14, 2010 11:49 AM Added CellTemplate
    Friday, May 14, 2010 11:45 AM
  • Hi kovman,

    I am doing very similar thing as you. My application shows in the DataGrid a matrix - list of lists. Each cell represents class instance by UserControl. The userControl's DataContext property looks to class instance stored in the matrix. But when I will change instance in the list of that matrix, the user controls do not know that their DataContext have to be changed.
    I have exchanged the string of cell item by new class CellItem which implements INotifyPropertyChanged and exposes property TheValue which is string.
    When I will to the solution sample above add button1, make the varriable vm private for whole class and do this in button click event:

    private void button1_Click(object sender, RoutedEventArgs e)
            {
                vm.Matrix[0].Values[0].TheValue = "NewText";
                vm.Matrix[0].Values[1] = new CellItem("SASA");
                vm.Matrix[0].Values[2] = null;
            }

    The cell 0,0 changes from "Col0" to "NewText". OK, no problem. But the user control which covers 0,1 do not change their DataContext property value and do not change the visuals.

    I can not find the right way to achieve this. Can you show me the solution how to do it?
    When I will unset and then set window DataContext manualy, it reflects the changes in the matrix, but it is not that solution I want. Hope it could be done in different way.

    Sources: (MainWindow and MyUserControl)

    this is the MainWindow.xaml

    <Window x:Class="TestApp1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
            xmlns:TestApp1="clr-namespace:TestApp1" Title="MainWindow" Height="350" Width="525">
        <Window.Resources>
            <DataTemplate x:Key="CellTemplate2">
                <TestApp1:MyUserControl1 />
            </DataTemplate>
        </Window.Resources>
        <Grid>
            <DataGrid ItemsSource="{Binding Path=Matrix}" AutoGenerateColumns="False" Height="239" HorizontalAlignment="Left" Margin="12,36,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="453" />
            <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="322,7,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
        </Grid>
    </Window>

    this is the MainWindow.xaml.cs

    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.ComponentModel;

    namespace TestApp1
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            private MainViewModel vm;

            public MainWindow()
            {
                InitializeComponent();
                vm = new MainViewModel();
                this.DataContext = vm;

                for (int i = 0; i < 10; i++)
                {
                    DataGridTemplateColumn col = new DataGridTemplateColumn();
                    col.Header = i.ToString();

                    FrameworkElementFactory fef = new FrameworkElementFactory(typeof(ContentPresenter));
                    Binding binding = new Binding();
                    //binding.Mode = BindingMode.TwoWay;
                    //binding.NotifyOnTargetUpdated = true;
                    fef.SetBinding(ContentPresenter.ContentProperty, binding);
                    fef.SetValue(ContentPresenter.ContentTemplateProperty, Resources["CellTemplate2"] as DataTemplate);

                    binding.Path = new PropertyPath("Values[" + i + "]");
                    DataTemplate dataTemplate = new DataTemplate();
                    dataTemplate.VisualTree = fef;
                    col.CellTemplate = dataTemplate;

                    dataGrid1.Columns.Add(col);
                }
            }

            private void button1_Click(object sender, RoutedEventArgs e)
            {
                vm.Matrix[0].Values[0].TheValue = "NewText";
                vm.Matrix[0].Values[1] = new CellItem("AnotherText");
                vm.Matrix[0].Values[2] = null;
                //
                //dataGrid1.DataContext = null;
                //dataGrid1.DataContext = vm;
            }
        }

        public class MainViewModel
        {

            private Row[] matrix;
            public Row[] Matrix { get { return matrix; } }

            public MainViewModel()
            {
                matrix = new Row[10];
                for (int i = 0; i < 10; i++)
                {
                    matrix[i] = new Row("Row" + i.ToString());
                }
            }
        }

        public class CellItem : INotifyPropertyChanged
        {
            public CellItem(string theValue)
            {
                this.theValue = theValue;
            }
            private string theValue;
            public string TheValue
            {
                get { return theValue; }
                set { theValue = value;
                    NotifyPropertyChange("TheValue");
                }
            }

            #region INotifyPropertyChanged Members

            public event PropertyChangedEventHandler PropertyChanged;

            private void NotifyPropertyChange(string propertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }

            #endregion
        }

        public class Row
        {
            private string name;
            private CellItem[] values;

            public string Name { get { return name; } }
            public CellItem[] Values { get { return values; } }

            public Row(string name)
            {
                this.name = name;
                values = new CellItem[10];
                for (int i = 0; i < 10; i++)
                {
                    values[i] = new CellItem("Col" + i.ToString());
                }
            }
        }
    }

    The MyUserControl.xaml:

    <UserControl x:Class="TestApp1.MyUserControl1"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 mc:Ignorable="d" 
                 d:DesignHeight="71" d:DesignWidth="88" DataContextChanged="UserControl_DataContextChanged">
        <Grid Height="45" Width="50">
            <Button Content="{Binding Path=TheValue, Mode=TwoWay}" Height="23" HorizontalAlignment="Left" Margin="1,12,0,0" Name="button1" VerticalAlignment="Top" Width="47" Click="button1_Click" />
        </Grid>
    </UserControl>

    The MyuserControl.xaml.cs:

    using System.Windows.Controls;

    namespace TestApp1
    {
        /// <summary>
        /// Interaction logic for MyuserControl1.xaml
        /// </summary>
        public partial class MyUserControl1 : UserControl
        {
            public MyUserControl1()
            {
                InitializeComponent();
            }

            private void button1_Click(object sender, System.Windows.RoutedEventArgs e)
            {
                button1.Content = DataContext == null ? "null" : "binding";
            }

            private void UserControl_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
            {
                button1.Content = "DC changed";
            }
        }
    }


    • Edited by Petr Škaloud Tuesday, July 10, 2012 3:16 PM correcting namespaces
    Tuesday, July 10, 2012 3:12 PM
  • Hi Petr,

    as I see the problem the behaviour si understandable and expectable. When you change the exposed property TheValue, the NotifyPropertyChanged is raised and the binding changes the visual.

    When you replace the object - no event is raised for binding and nothing goes no. When you unset and set the DataContext all objects are binded again and the grid is repainted well - but it's repainted all instead of one changed value.

    I think that if you don't have any special reason, the first approach of setting the exposed property is the best. The reason is that unsetting and setting the DataContext is expensive for rendering. If you even come with the solution how to raise the correct event after the object is replaced, it has to be wired with DataTemplate and the whole control has to be repaint at all. With one label - not a problem, with more labels or some advanced controls it might became also expensive operation. Setting just the TheValue supose to invalidate just the changed part and redraw just it.

    I hope this will help you

    Lubomir Kovac (kovman)

    Tuesday, July 10, 2012 7:19 PM
  • Hi kovman,

    at first thanks for your reply and explanation.

    It look that it could be solved by code behind actions.
    In case of my project the matrix can include not initialized cells, so, the matrix values could be null. The user control instances are created by WPF but theirs data context property are set to null in that case.

    When the application will create new instance in matrix position, I have to update corresponding user control DataContext property.

    And it is done by this code (it extends button1_Click method from my previous post):

                

            private void button1_Click(object sender, RoutedEventArgs e)
            {
                vm.Matrix[0].Values[0].TheValue = "SomeText";
                vm.Matrix[0].Values[1] = new CellItem("AnotherText");
                vm.Matrix[0].Values[2] = null;
                //
                DataGridCell cell;
                MyUserControl1 uc;
                // Setup cell conten for cell 0,1
                cell = GetCell(dataGrid1, 0, 1);
                uc = GetVisualChild<MyUserControl1>(cell);
                uc.DataContext = vm.Matrix[0].Values[1];
                // Setup cell conten for cell 0,2
                cell = GetCell(dataGrid1, 0, 2);
                uc = GetVisualChild<MyUserControl1>(cell);
                uc.DataContext = vm.Matrix[0].Values[2];

                // Test binding and notification -> change value of new CellItem instance
                vm.Matrix[0].Values[1].TheValue = "BIG Text";

            }

    It uses methods such is GetCell and GetVisualChild. These methods I found on the web here and put them into MainWindow.xaml.cs:
    http://blogs.msdn.com/b/vinsibal/archive/2008/11/05/wpf-datagrid-new-item-template-sample.aspx

    The article is focused to different problem, but it uses methods which I can share.

    So, my problem is solved and I am satisfied. Of course, I have to handle nulls and so on, but the principle is clear.
    Thanks again.

    Souce code of GetCell, GetRow and GetVisualChild methods:

            public static DataGridCell GetCell(DataGrid dataGrid, int row, int column)
            {
                DataGridRow rowContainer = GetRow(dataGrid, row);
                if (rowContainer != null)
                {
                    DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(rowContainer);

                    // try to get the cell  but it may possibly be virtualized
                    DataGridCell cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(column);
                    if (cell == null)
                    {
                        // now try to bring into view and retreive the cell
                        dataGrid.ScrollIntoView(rowContainer, dataGrid.Columns[column]);

                        cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(column);
                    }

                    return cell;
                }

                return null;
            }

            public static DataGridRow GetRow(DataGrid dataGrid, int index)
            {
                DataGridRow row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(index);
                if (row == null)
                {
                    // may be virtualized, bring into view and try again
                    dataGrid.ScrollIntoView(dataGrid.Items[index]);
                    dataGrid.UpdateLayout();

                    row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(index);
                }

                return row;
            }

            public static T GetVisualChild<T>(Visual parent) where T : Visual
            {
                T child = default(T);

                int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
                for (int i = 0; i < numVisuals; i++)
                {
                    Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
                    child = v as T;
                    if (child == null)
                    {
                        child = GetVisualChild<T>(v);
                    }
                    if (child != null)
                    {
                        break;
                    }
                }

                return child;
            }

    • Edited by Petr Škaloud Wednesday, July 11, 2012 7:29 AM Added source code of mentioned methods
    Wednesday, July 11, 2012 7:09 AM