Tuesday, May 26, 2009

Dependency properties and collections combined with XAML

I could have run this post in my Adventures while building a Silverlight Enterprise Application series, but I found it to become to easy that way, so I posted it with an actual title :-)
I did ran into this while working on the Selector control as mentioned in part #11 of this series, but it's such a general pitfall, I figured it makes sense to put it outside the series.

As you may remember from this article, I placed two datagrids inside a usercontrol to allow users to select items by moving them from one list to another. As many of you already know, one of the features of the DataGrid control is the option to specify your own columns trough the Columns property. I wanted the users of my control to have the same functionality.

To make everyones lives as easy as possible, I thought I'd simply copy the DataGrid behaviour for this. I dug into the documentation and found that the DataGrid.Columns property is in fact a ObservableCollection of DataGridColumn objects. Thus I defined a dependency property of this type. I have pasted some example code below, but before you dive into that, please let me state that I always use the propdp code snippet for dependency properties and I don't find it useful to clean it up as I consider it to be generated code. If anyone disagrees, please let me know (and why, off course).

So, the dependency property code looked like this:

public ObservableCollection<DataGridColumn> Columns
{
get { return (ObservableCollection<DataGridColumn>)GetValue(ColumnsProperty); }
set
{
SetValue(ColumnsProperty, value);
UpdateColumns();
}
}

// Using a DependencyProperty as the backing store for Columns. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.Register("Columns", typeof(ObservableCollection<DataGridColumn>), typeof(Selector), new PropertyMetadata(null));


The UpdateColumns that is called from the properties setter simply copies all the columns from the Columns property into the Columns properties of either DataGrids. So I figured I would try this out in XAML. The following code snippet is slightly altered to conceal some of the details of the underlying project.

<controls:Selector x:Name="stringsSelector"
SourceHeaderText="My source header"
TargetHeaderText="My target header"
>
<controls:Selector.Columns>
<data:DataGridTextColumn />
</controls:Selector.Columns>
</controls:Selector>

As you can see I simply tried to add a DataGridTextColumn to my Columns collection. As I run this the InitializeComponent() method of the page fails with a parse exception, stating that there is some invalid property (any Silverlight developer knows how useful these are :-( ). After I then stop the debugger, Visual Studio does put a blue line under the DataGridTextColum tag in the XAML, but also with the same exception. It doesn't make much sense as the DataGridTextColumn class should be valid in this position and I do hope some better exception handling will be introduced for problems like these.

At first I thought that it may have something to do with the different namespaces and how they have to work together, but I ruled this out by building a small trial application with a similar scenario. I figured I would also add a collection variant to this application and it worked as well! Then I thought, maybe it had something to do with the DataGridColumn and I added that as well and it worked! Then I went in and compared the dependency properties and I found out that I had changed the default value of my dependency property in the trial application. I initialized an empty collection there, in stead of setting it to null by default. I changed this in my trial application and it failed with the parser exception again. So here is the changed dependency property as it now works.

public ObservableCollection<DataGridColumn> Columns
{
get { return (ObservableCollection<DataGridColumn>)GetValue(ColumnsProperty); }
set
{
SetValue(ColumnsProperty, value);
UpdateColumns();
}
}

// Using a DependencyProperty as the backing store for Columns. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.Register("Columns", typeof(ObservableCollection<DataGridColumn>), typeof(Selector), new PropertyMetadata(new ObservableCollection<DataGridColumn>()));


Conclusion: the dependency property gets accessed by the parser well before it gets set and apparently some member is accessed as well, causing a null reference exception, which is translated into a parser exception. By simply creating an empty instance this goes away. A decent exception would have saved me several hours of troubleshooting, so I hope Microsoft handles this in future releases.

UPDATE: Microsoft has put in place a more descriptive error message in this particular case. Also I should point out that calling code from the property setter is, in the case of a dependency property, not correct, as the setter is not called when accessed from XAML. To fix that you should implement a PropertyChangedCallback delegate and pass that as an argument to the PropertyMetadata. That does not change the fact that you should still provide an instance as a default value for this dependency property.

Thanks for reading in again and I hope you found this article useful. If you have any questions or remarks, please leave them below. I always enjoy reading them and replying whenever needed.

5 comments:

  1. Hi Jonathan,

    The idea of setting columns as a property is what I am looking for... however I can't get your example to work. I have a 'parent' user control with a text label and datagrid. The child control is able to set the text label but not the datagrid columns. Everything compiles and runs fine. I tried setting parent grid columns to empty or defined. Child is never able to override. Any ideas? Thanks!

    Parent
    -------

    public ObservableCollection&ltDataGridColumn&gt Columns
    {
    get { return (ObservableCollection&ltDataGridColumn&gt)GetValue(ColumnsProperty); }
    set
    {
    SetValue(ColumnsProperty, value);
    MessageBox.Show("Hello world"); // never called ?
    // UpdateColumns();
    }
    }

    // Using a DependencyProperty as the backing store for Columns.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ColumnsProperty =
    DependencyProperty.Register("Columns", typeof(ObservableCollection&ltDataGridColumn&gt),
    typeof(parentControl), new PropertyMetadata(new ObservableCollection&lt DataGridColumn&gt()));


    Child
    --------

    &ltlocal:parentControl LabelHeader="This is child text!" &gt
    &ltlocal:parentControl.Columns&gt

    &lttoolkit:DataGridTextColumn Width="Auto" Header="Child" Binding="{Binding Path=.}" /&gt

    &lt/local:parentControl.Columns&gt

    &lt/local:parentControl&gt

    Parent XAML (uninteresting)
    --------------------------------

    &ltLabel Style="{StaticResource GridHeader}" Grid.Column="0" Content="{Binding ElementName=myUcName, Path=LabelHeader}"/&gt

    &lttoolkit:DataGrid x:Name="myGrid"
    Style="{StaticResource DataGridStyle}"
    ItemsSource= "{Binding Path=.}"&gt
    &lt/toolkit:DataGrid&gt

    ReplyDelete
  2. Hi amackay99,

    Is the Columns set called? There is comment there suggesting it might not get called.
    Also could you try and set a new ObservableCollection<Column> instance to the Columns property in code? It should contain at least one column so you can see if it does anything.

    Please let us know what you find, so we can help you along.

    Greets,
    Jonathan

    ReplyDelete
  3. Jonathan,

    Yes, it seems the set is never called (sorry for the formatting). I did the following and the 'set' method fires...
    gridColumns = new ObservableCollection_lt_DataGridColumn_gt_();

    It makes sense that putting the columns in xaml is the same as:
    gridColumns.Add(new DataGridTextColumn { Header = "abc" });
    gridColumns.Add(new DataGridTextColumn { Header = "efg" });

    It would be great to somehow override the 'add' method and include logic to add the column to the grid.

    What I have done to make this work is to call my 'UpdateColumns' method in my window contructor. This loops through the collection, adding each column that was defined in the xaml. It isn't pretty but seems to work. I'll continue to ponder.

    public void UpdateColumns()
    {
    if (gridColumns.Count > 0)
    myGrid.Columns.Clear();

    foreach (DataGridColumn c in gridColumns)
    {
    myGrid.Columns.Add(c);
    }
    }

    Thanks
    Andy

    ReplyDelete
  4. Ok...if I add a handler for 'CollectionChanged' event, this seems to work well:

    gridColumns.CollectionChanged += new NotifyCollectionChangedEventHandler(gridColumns_CollectionChanged);
    .
    .
    .
    void gridColumns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {

    if (e.Action.Equals(NotifyCollectionChangedAction.Add))
    {
    myGrid.Columns.Add(((ObservableCollection_DataGridColumn_)sender)[e.NewStartingIndex]);
    }
    }

    ReplyDelete