views:

243

answers:

2

I have to create a XamDataGrid that shows a dynamic amount of columns for a time frame x to y. Therefore I dont know how many years a user would select to have these columns created upfront.

Now usualy within MVVM you would just populate the data through as many Properties as you would need Columns within your XamDataGrid and the latter would just autogenerate them.

Obviously I couldn't just create Properties within my ViewModel at runtime unless I did something crazy with Reflection.

How else would I achieve this?

Should I just create unbound fields for the datagrid and fill them through code? I agree I wont need a two way Binding at this stage, since the grid is only readonly...just thinking loud.

Is this approach ok without violating teh MVVm pattern? Thanks

+1  A: 

You can use indexers:

In your ViewModel:

public MyVariableCollection RowData
{
    get { return new MyVariableCollection(this); }
}

In MyVariableCollection: protected SomeRowViewModel viewModel;

public MyVariableCollection(SomeRowViewModel viewmodel)
{
    this.viewModel = viewmodel;
}

public object this[string name]
{
    get { return viewModel.GetRowColumnValue(name); }
}

I've tried to keep it brief: but the idea is, you have a new class with an indexer defined, then you can bind like this:

{Binding Path=SomeRowViewModelInstance.RowData["ColumnName"]}

The column collection on the data grid control would be bound - and you could set a column template for each column to bind to the column in question; you needn't use a literal string in the indexer like that.

Hope that provides some food for thought - any questions on this route please leave a comment.


Edit for an additional thought: I have used the contents ComponentModel namespace to produce a custom TypeDescriptor. It is fairly in depth but you can make an object 'appear' to have additional or custom properties. It's far more complex than the indexer method I posted above but if you get stuck it's worth a look.

Kieren Johnstone
Thanks for your great response Kieren. I found three problems with your model though. First is the RowData Property of ViewModel, that is passing in its own ctor a 'this'. However the actual ctor accepts a type of SomeRowViewModel that is not compatible with the type of previous 'this'.
Kave
The second problem is the way you bind a single column. Within XamDataGrid you would usually bind the whole xamdatagrid simply to a BindingList<SomeRowVieModel> collection an dit would autogenerate those rows. Unless you create the columns manually, you wouldnt really be able to bind to each column seperately.
Kave
The third problem is the following, I could autogenerate the columnNames dynamically for timeperiod x to y, so that I could use them in the indexer as you suggested. But the binding cant still be on the static xaml code. More it has to be in the code behind of the xaml view in order to use the column names in a variable fashioned way. Unless I am missing here something...Thanks
Kave
Hi Kave. The gist of it is: you still bind to a collection of ViewModel objects. But rather than bind each column to a property, you bind it to the `RowData["ColumnName"]` property/indexer on the ViewModel representing the row. The advantage with this approach is you can use another binding/property path to specify the column name. As for binding the column collection, I am referring to the Columns property on a normal grid - think Infragistics might call them Fields. Does that help?
Kieren Johnstone
(To come at it from another angle, you say 'I couldn't just create Properties within my ViewModel at runtime unless I did something crazy with Reflection.'. I'm saying 'you can do that using an indexer - rather than obj.Property use obj["Property"].'. And if that's no good, you can make an object seem to have varying properties, using a CustomTypeDescriptor as I alluded to at the end.
Kieren Johnstone
I am sorry Kieren, I still dont get this to work. Do you have a simple example by any chance?
Kave
Not an example specific to the XamDataGrid I'm afraid. I've done a similar thing using indexers with my own custom GridView/ListView view system, which has variable columns bound dynamically. There will be a property on the grid which should be bound to a collection of columns; you should bind that to your list of columns - and for each column template you should bind to an indexer property which retrieves data for each column; then bind the main data property as normal. Sorry I can't be any more specific than that.
Kieren Johnstone
A: 

I had a similar problem because the user was able to define the columns of the grid at runtime.

I wrote a control containing the xam datagrid and exposing a DataSource dependency property to bind the model for the grid (i.e. a data table).

Every time the source changed (you can add event listeners for PropertyChanged and the grids FieldLayoutInitializing event) the grid was dynamically re-rendered by clearing its datasource and resetting it:

private void ReRenderGrid()
{
    XamDataGrid.FieldLayouts.Clear();
    XamDataGrid.ClearValue(DataPresenterBase.DataSourceProperty);
    XamDataGrid.DataSource = DataSource.Data.DefaultView;
}

The columns are re configured by an event handler on the following event which is raised by xamdatagrid after the grids datasource is reset:

XamDataGrid.FieldLayoutInitializing += LayoutInitializing;

Handler:

private void LayoutInitializing(object sender, FieldLayoutInitializingEventArgs e)
{
    const string deletebuttonstyle = "DeleteButtonStyle";
    const string requiredinputvalue = "RequiredInputValue";
    const string optionalinputvalue = "OptionalInputValue";
    const string outputvalue = "OutputValue";

    var fieldLayout = e.FieldLayout;
    fieldLayout.Fields.Clear();

    AddFields(DataSource.InColumns, requiredinputvalue, fieldLayout);
    AddSplitter(fieldLayout);
    AddFields(DataSource.OptionalInColumns, optionalinputvalue, fieldLayout);
    AddSplitter(fieldLayout);
    AddFields(DataSource.OutColumns, outputvalue, fieldLayout);

    AddUnboundField(fieldLayout, string.Empty, GetStyle(deletebuttonstyle));
}

In my case the datasource contained all columns the user configured. AddFields calls this method for each list entry:

private void AddField(string name, Style style, FieldLayout fieldLayout)
{
    var field = new Field {Name = name};
    field.Settings.LabelPresenterStyle = style;
    field.Settings.CellValuePresenterStyle = GetStyle("StandardCellValueStyle");
    fieldLayout.Fields.Add(field);
}

AddSplitter and AddUnboundField are implemented in a similar fashion.

Zebi
Zebi, thank you very much for these code snipppets. I am sure its working and it looks indeed promising. However these are just code snippets. I cant follow the states.XamDataGrid.DataSource = DataSource.Data.DefaultView; What is Data in this case, I seem not to have it.The Method AddFields seem to have different signature than your AddField. It is really not clear yet to me how this should work. A simple working example would really be appreciated.
Kave
The DataSource Property in my example refers to a custom Type I made holding the data table (.Data property) and the columns the user defined (he can define 3 types of cols, input, optional input and output cols). You have to have some Data Table to use as datasource for your grid. Reset this table.I can not create a working example right now because the code is from a project at work. I'll see if I can do tomorrow.
Zebi
Thanks Zebi, highly appreciated.
Kave