0

All, I am creating a data set which is 'bound' to a DataGrid at run-time. I pull in some data that is used to build a class which is inheriting from ObservableCollection<T>. The class in question is

public class ResourceData : ObservableCollection<ResourceRow>
{
   public ResourceData(List<ResourceRow> resourceRowList) : base()
   {
      foreach (ResourceRow row in resourceRowList)
         Add(row);
   }
}

public class ResourceRow
{
   private string keyIndex;
   private string fileName;
   private string resourceName;
   private List<string> resourceStringList;

   public string KeyIndex
   {
      get { return keyIndex; }
      set { keyIndex = value; }
   }

   public string FileName
   {
      get { return fileName; }
      set { fileName = value; }
   }

   public string ResourceName
   {
      get { return resourceName; }
      set { resourceName = value; }
   }

   public List<string> ResourceStringList
   {
      get { return resourceStringList; }
      set { resourceStringList = value; }
   }
}

I return a ResourceData object from a method called BuildDataGrid(), defined as

private ResourceData BuildDataGrid(Dictionary<string, string> cultureDict)
{
    // ...
}

I then set the DataGrid's ItemSource to this ResourceData object via

dataGrid.ItemSource = BuiltDataGrid(cultureDictionary);

however, this is not correctly expanding the ResourceStringList within ResourceRow. I get displayed:

The DataGrid

My question is: How can I amend my ResourceRow class to allow the DataGrid to automatically read and display the contents of a ResourceData object? I want to display each item in the ResourceStringList in a separate column.

Thanks for your time.

MoonKnight
  • 23,214
  • 40
  • 145
  • 277
  • Will each of the resourceStringList objects contain the same number of elements? If so, it might be easiest to just enumerate the columns you want to add from the resource string list in the ResourceRow object. If the number of strings in the collection varies between ResourceRow objects, the problem becomes more difficult since you will have a mixture of design-time columns and run-time columns which will require code-behind to mix together – Andrew Apr 18 '13 at 22:05
  • For each load, yes, the `resourceStringList` object will contain the same number of items. I am using a List here to avoid the requirement to initialise if I were using an array... – MoonKnight Apr 18 '13 at 22:08
  • so you want to show the `ResourceStringList` as extra columns or just display them as a editable list inside the cell? – sa_ddam213 Apr 18 '13 at 22:24
  • As extra columns. My plan was to have the standard strings as columns, but seeing as some data is unknown until run-time I put this stuff in a list... – MoonKnight Apr 18 '13 at 22:25

3 Answers3

1

Here's my solution - I changed up the control, rather than the ResourceRow but I think it achieves what you're looking for.

We just create a DataGrid with design-time columns in the xaml, and then add the run-time columns dynamically in the control's constructor. A couple things to keep in mind - we're assuming the first row of the ResourceData is a good model for all of the rows since this is the one we use to determine the columns to add to the datagrid in the constructor. Second, Since ResourceRow does not implement INotifyPropertyChanged, we won't get updated values for the columns that come from the ResourceStringList - but that can easily be changed.

The Code-behind:

namespace WpfApplication
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            ResourceData data = GetData();
            _dataGrid.ItemsSource = data;

            for (int i = 0; i < data[0].ResourceStringList.Count; i++)
            {
                DataGridTextColumn column = new DataGridTextColumn();
                column.Binding = new Binding(string.Format("ResourceStringList[{0}]", i));
                column.Header = string.Format("dynamic column {0}", i);
                _dataGrid.Columns.Add(column);
            }
        }

        public ResourceData GetData()
        {
            List<ResourceRow> rows = new List<ResourceRow>();

            for (int i = 0; i < 5; i++)
            {
                rows.Add(new ResourceRow() { KeyIndex = i.ToString(), FileName = string.Format("File {0}", i), ResourceName = string.Format("Name {0}", i), ResourceStringList = new List<string>() { "first", "second", "third" } });
            }
            return new ResourceData(rows);
        }
    }

    public class ResourceData : ObservableCollection<ResourceRow>
    {
        public ResourceData(List<ResourceRow> resourceRowList)
            : base()
        {
            foreach (ResourceRow row in resourceRowList)
                Add(row);
        }
    }

    public class ResourceRow
    {
        private string keyIndex;
        private string fileName;
        private string resourceName;
        private List<string> resourceStringList;

        public string KeyIndex
        {
            get { return keyIndex; }
            set { keyIndex = value; }
        }

        public string FileName
        {
            get { return fileName; }
            set { fileName = value; }
        }

        public string ResourceName
        {
            get { return resourceName; }
            set { resourceName = value; }
        }

        public List<string> ResourceStringList
        {
            get { return resourceStringList; }
            set { resourceStringList = value; }
        }
    }
}

The xaml:

<Window x:Class="WpfApplication.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 x:Name="_dataGrid" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding KeyIndex}" Header="Key Index"/>
                <DataGridTextColumn Binding="{Binding FileName}" Header="File Name"/>
                <DataGridTextColumn Binding="{Binding ResourceName}" Header="Resource Name"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>
Andrew
  • 1,482
  • 9
  • 16
  • That's fantastic! Thanks very much for your time. I am trying to learn WPF in my spare time (after work and weekend and it is killing me!). I am confused though, the binding for the columns in your XAML references 'KeyIndex', how does it know what 'KeyIndex' is here? – MoonKnight Apr 18 '13 at 22:47
  • 1
    Glad I could help! If this effectively solved your problem, please mark it as the accepted solution! WPF uses a lot of reflection to resolve bindings to the underlying property at run-time. In the xaml, you can think of KeyIndex as a magic word that will get resolved to some property on the run-time data context when the control is created. If I had mispelled KeyIndex, the column would still be created but it wouldn't have any data in it since the reflection API wouldn't find an appropriate property retrieve. – Andrew Apr 18 '13 at 22:51
  • Oh right, that is awesome. Again cheers for your time... Of course I will mark it as the answer... I am trying to solve another problem with this entire approach, which is restricting me in many ways. I have asked another question [here](http://stackoverflow.com/questions/16063077/how-to-bind-data-to-datagrid-at-run-time), and there is an ongoing thread at [MSDN](http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/613a9a1e-564f-4fe4-9316-7736678284a3/#cf875119-7998-430c-b35c-6c54c8db8d9d) I would be massively appreciative if you could aid me here... – MoonKnight Apr 18 '13 at 22:56
  • Please let me know if I am taking this pi$$! Anyway, thanks again and all the best. – MoonKnight Apr 18 '13 at 22:58
0

Your properties need to inherit from INotifyPropertyChanged, so they can update the UI, and so does your ViewModel too. Have your view model (or gasp! code behind) inherit from INotifyPropertyChanged. Implement the interface then you can do this for your properties.

    private string _keyIndex= string.Empty;
    public string KeyIndex
    {
        get { return this._keyIndex; }

       set
       {
            this._keyIndex= value;
            this.RaisePropertyChangedEvent("KeyIndex");
        }
   }
Jeff
  • 972
  • 5
  • 11
  • Why do they? See [here](http://msdn.microsoft.com/en-GB/library/ms668604.aspx) for implementation details of `ObservableCollection`. My question is about how to make the `DataGrid` auto-enumerate the `List` of the `ResourceRow` class - thanks for your time. – MoonKnight Apr 18 '13 at 21:45
0

@Andrew's answer is correct, but I like simplicity and ease of use so I implemented a way for the columns to be automatically set up and bound to the datagrid with the names I wanted without have to use the datagrid columns.

Declare a column name class that inherits from attribute like so:

public class ColumnNameAttribute : Attribute
{
    public ColumnNameAttribute(string Name) { this.Name = Name; }
    public string Name { get; set; }

}

Decorate your model with the Column Name like so:

public class Data
  {
        [ColumnName("Name")]
        public string Name{ get; set; }
        [ColumnName("Customer")]
        public string Client { get; set; }
        [ColumnName("Activity Type")]
        public string Activity_Type { get; set; }
    }

Set AutoGenerateColumns to True and then add this event handler:

 private void gen_AutoGridColumns(object sender, DataGridAutoGeneratingColumnEventArgs e)
    {           
        var desc = e.PropertyDescriptor as PropertyDescriptor;
        if (desc.Attributes[typeof(ColumnNameAttribute)] is ColumnNameAttribute att) e.Column.Header = att.Name;

        //set the width for each column           
        e.Column.Width = new DataGridLength(1, DataGridLengthUnitType.Star);            
    }

You now can skip adding the DataGridColumns definitions into the DataGrid and still have them named whatever you want.

MattE
  • 1,044
  • 1
  • 14
  • 34