3

I want to show data in a datagrid where the data is a collection of

public class Thing
{
    public string Foo { get; set; }
    public string Bar { get; set; }
    public List<Candidate> Candidates { get; set; }
}

public class Candidate
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    ...
}

where the number of candidates in Candidates list varies at runtime.

Desired grid layout looks like this

Foo | Bar | Candidate 1 | Candidate 2 | ... | Candidate N

I'd like to have a DataTemplate for each Candidate as I plan changing it during runtime - user can choose what info about candidate is displayed in different columns (candidate is just an example, I have different object). That means I also want to change the column templates in runtime although this can be achieved by one big template and collapsing its parts.

I know about two ways how to achieve my goals (both quite similar):

  1. Use AutoGeneratingColumn event and create Candidates columns
  2. Add Columns manually

In both cases I need to load the DataTemplate from string with XamlReader. Before that I have to edit the string to change the binding to wanted Candidate.

Is there a better way how to create a DataGrid with unknown number of DataGridTemplateColumn?

Note: This question is based on dynamic datatemplate with valueconverter

Edit: As I need to support both WPF and Silverlight, I've created my own DataGrid component which has DependencyProperty for bindig a collection of columns. When the collection changes, I update the columns.

Community
  • 1
  • 1
Lukas Cenovsky
  • 5,476
  • 2
  • 31
  • 39

3 Answers3

2

For example we create 2 DataTemplates and a ContentControl:

<DataTemplate DataType="{x:Type viewModel:VariantA}"> <dataGrid...> </DataTemplate>
<DataTemplate DataType="{x:Type viewModel:VariantB}"> <dataGrid...> </DataTemplate>

<ContentControl Content="{Binding Path=GridModel}" />

Now if you set your GridModel Property (for example type object) to VariantA or VariantB, it will switch the DataTemplate.

VariantA & B example Implementation:

public class VariantA
{
    public ObservableCollection<ViewModel1> DataList { get; set; }
}

public class VariantB
{
    public ObservableCollection<ViewModel2> DataList { get; set; }
}

Hope this helps.

Marco B.
  • 765
  • 1
  • 8
  • 14
0

I don't know if this is a "better" way, since this remains pretty ugly, but I personnaly did like this:

  • make the template in xaml
  • use a multibind that takes the current binding + a binding to the column to get the "correct" dataContext (i.e.: the cell instead of the row)
  • use a converter on this binding to get the value of the property you like, an optionally add a parameter if you have many properties to retrieve.

e.g.: (sorry, I did not adapt my code to suit your project, but you should be able to do it yourself from there)

here is my dataTemplate:

<DataTemplate x:Key="TreeCellTemplate">
    <Grid>
        <TextBlock HorizontalAlignment="Left" VerticalAlignment="Center" Margin="5,0,0,0">
            <TextBlock.Text>
                <MultiBinding Converter="{StaticResource RowColumnToCellConverter}" ConverterParameter="Text">
                    <Binding />
                    <Binding RelativeSource="{RelativeSource AncestorType=DataGridCell}" Path="Column" />
                </MultiBinding>
            </TextBlock.Text>
        </TextBlock>
    </Grid>
</DataTemplate>

and here is my converter:

   public class RowColumnToCellConverter : MarkupExtension, IMultiValueConverter
   {
      public RowColumnToCellConverter() { }

      public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
      {
         XwpfRow row = values[0] as XwpfRow;
         XwpfTreeColumn column = values[1] as XwpfTreeColumn;

         if (row == null || column == null) return DependencyProperty.UnsetValue;

         TreeCell treeCell = (TreeCell)row[column.DataGrid.Columns.IndexOf(column)];
         switch ((string)parameter)
         {
            case "Text": return treeCell.Text;
            case "Expanded": return treeCell.Expanded;
            case "ShowExpandSymbol": return treeCell.ShowExpandSymbol;
            case "CurrentLevel": return new GridLength(treeCell.CurrentLevel * 14);

            default:
               throw new MissingMemberException("the property " + parameter.ToString() + " is not defined for the TreeCell object");
         }
      }

      public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
      {
         throw new NotSupportedException();
      }

      public override object ProvideValue(IServiceProvider serviceProvider)
      {
         return new RowColumnToCellConverter();
      }
   }

this saves the MVVM model, and I prefer this way of doing things because I really dislike using xaml parsers to make "dynamic" datatemplates, but it's still an ugly Hack from my point of view.

I wish the guys at MS would give us a way to get cells instead of rows as dataContexts to be able to generate templated columns on the fly...

hope this helps

EDIT: In your case, the converter ought to be a lot simpler actually (you can return the cell's instance directly if I'm not mistaken, and you don't need any parameter), but I left the more complex version nonetheless, just in case somebody else has a similar issue

David
  • 6,014
  • 4
  • 39
  • 55
  • Interesting idea although I cannot use it as Silverlight does not support `MultiBinding`. – Lukas Cenovsky Feb 08 '11 at 20:03
  • arf, sorry about that, I'm not familiar with silverlight so I did not know. I can confirm that this works pretty well in WPF though, I used it again today and start to be quite happy with the method, although it's ugly. – David Feb 08 '11 at 20:38
0

I've been looking at a similar problem and have only found a handful of useful patterns. The whole 'dynamic column' problem is an interesting one in silverlight.

Yesterday I found this page Silverlight DataGrid with Dynamic Columns on Travis Pettijohn's site during my searches.

Previously I'd been using the 'index converter' pattern outlined by Colin Eberhardt which works fantastically well... as long as you use DataGridTextColumn. Everything can be done in code behind, and I had no trouble applying styles at run time. However my requirement is now to apply some 'cell level' formatting - change the background for the cell, etc - which means a DataGridTemplateColumn is required.

The big problem with a DataGridTemplateColumn for me was that I can't set the binding in code. I know we can build it by parsing xaml, but like everyone else that seems like a massive hack and unmaintainable to the nth.

The pattern outlined by Travis (the first link above) is completely different. At 'run time' (i.e. page load time), create the columns you need in your grid. This means iterate through your collection, and add a column for each item with the appropriate header etc. Then implement a handler for the RowLoaded event, and when each row is loaded simply set the DataContext for each cell to the appropriate property / property index of the parent.

private void MyGrid_RowLoaded(object sender, EventArgs e)
{
    var grid = sender as DataGrid;
    var myItem = grid.SelectedItem as MyClass;
    foreach (int i = 0; i < myItem.ColumnObjects.Count; i++)
    { 
        var column = grid.Columns[i]; 
        var cell = column.GetCellContent(e.Row)
        cell.DataContext = myItem.ColumnObjects[i];
    }
}

This has removed the need for me to use the index converter. You can probably use a Binding when setting the cell.DataContext but for me it's easier to have the template simply bind directly to the underlying object.

I now plan on having multiple templates (where each can bind to the same properties on my cell object) and switching between them at page load. Very tidy solution.

Kirk Broadhurst
  • 27,836
  • 16
  • 104
  • 169
  • This `RowLoaded` looks like a really good solution. Although I don't see it as a solution when you need to add and remove columns dynamically. Then it's probably better to load template from string and replace there `myItem.ColumnObjects[i]` binding with actual index. – Lukas Cenovsky Nov 21 '11 at 22:50
  • @LukasCenovsky I haven't posted the dynamic columns code but you can view it in the links. As I mentioned, that's the easy part - `foreach (var item in columnsToAdd){ grid.Columns.Add(new DataGridTemplateColumn(){ Header = item.HeaderProperty }; }` – Kirk Broadhurst Nov 21 '11 at 23:14
  • @LukasCenovsky Add the `DataTemplate`s that you want to use as `Resources` and reference them in code. I'll try to edit my answer later on... – Kirk Broadhurst Nov 21 '11 at 23:16
  • By dynamic I mean you have displayed the grid and you want to add/remove columns later. This solution means remove all columns and add them again, doesn't it? Or am I wrong? – Lukas Cenovsky Nov 23 '11 at 19:11
  • @LukasCenovsky That would be the easiest way to go, yes. But I don't think that reconstructing the whole grid would be such a big deal would it? – Kirk Broadhurst Nov 23 '11 at 21:48
  • I think it depends on templates. I had a performance problem when creating the datagrid - it took couple of seconds so I had to load columns in batches to not freeze the UI so much (like in those old WinForm days :). – Lukas Cenovsky Nov 24 '11 at 09:33
  • @LukasCenovsky WPF rendering is *much* better in .NET 4.0 (compared with 3.5), make sure you're using that if possible. – Kirk Broadhurst Nov 24 '11 at 11:13