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.

Wednesday, May 20, 2009

Adventures while building a Silverlight Enterprise application part #11

Wow, it's been a while. I've been on a holiday and right after that I became ill. Now that I'm back, some things have changed in the team (people leaving or getting different assignments, etc.). This had it's impact on how the team operates so I needed some time to let the dust settle, before I could take out some time to write my next post here.

For the next week or so, I'll be spending time on building a selector, or whatever I should call it. Basically it's a control that consists of two lists with items, where you can move items from one list to another and back. You've all seem them and a lot of you probably build them as well.

I just got started on the basic features and I already ran into some interesting snags on the way. I first started building the usercontrol with two listboxes, only to get reminded very quickly that they do not support multi select, which is a requirement for our control. After digging some more into the requirements I decided that the datagrid fits our needs better anyway, so I'm using two of those.

One requirement was that the control needs to be able to be used in two orientations, pretty much like the stackpanel. The two lists can be next to each other, with the buttons in between, or they can be above each other, with the buttons in between. I started building them side by side (or in horizontal orientation, if you will), using stackpanels as my container controls. I figured I could change the rotation of my stackpanels to rotate parts of my control. It worked great, except for the captions on the buttons. I would need to rotate these myself.
To do this I embedded a TextBlock element inside the buttons content and named it so I could access it from code. Then I created a RotateTransform object with a 90 degree angle and assigned it to the TextBlocks RenderTransform property, only to find out that my TextBlock would not be in the right place after this. It turns out that the RenderTransformOrigin property defaults to Point 0, 0, which is not right for our rotation. It should rotate around the center of the TextBlock, so it stays in the same place.
I figured that I should calculate a point that indicates the center of the TextBlock and so I set out to do so, only to find out it would completely mess up the location of the TextBlock. I couldn't figure out why, so I fired up Expression Blend to create the rotation in there and see what XAML would come of it. It turned out that the origin should be 0.5, 0.5, which indicates the center of the TextBlock, relative to it's size. Isn't Silverlight genius?

Unfortunately I can't post the full control (yet?), but here is the code for the rotation/orientation of the control:


private void ApplyOrientation()
{
layoutRootStackPanel.Orientation = Orientation;
if (Orientation == Orientation.Horizontal)
{
buttonsStackPanel.Orientation = Orientation.Vertical;
unselectItemsTextBlock.RenderTransform = null;
unselectAllItemsTextBlock.RenderTransform = null;
selectItemsTextBlock.RenderTransform = null;
selectAllItemsTextBlock.RenderTransform = null;
}
else
{
buttonsStackPanel.Orientation = Orientation.Horizontal;
RotateTransform rotateTransform = new RotateTransform();
rotateTransform.Angle = 90;
Point centerPoint = new Point(0.5, 0.5);
unselectItemsTextBlock.RenderTransformOrigin = centerPoint;
unselectItemsTextBlock.RenderTransform = rotateTransform;
unselectAllItemsTextBlock.RenderTransformOrigin = centerPoint;
unselectAllItemsTextBlock.RenderTransform = rotateTransform;
selectItemsTextBlock.RenderTransformOrigin = centerPoint;
selectItemsTextBlock.RenderTransform = rotateTransform;
selectAllItemsTextBlock.RenderTransformOrigin = centerPoint;
selectAllItemsTextBlock.RenderTransform = rotateTransform;
}
}


I hope this is helpful to you. Please leave me a comment (or a question if you like). I always enjoy them and I try to respond in a timely fashion.