Wednesday, February 2, 2011

IronPython & WPF: Data binding with TreeView's selected element

In this post I'm going to show a small example of using data binding with the selected element of a WPF TreeView with an IronPython class.

A couple of days ago I had the necessity of using data binding to keep track of the selected value of a WPF TreeView . At first it seemed to be an easy task so I wrote:

<TreeView SelectedValue="{Binding selected, Mode=TwoWay}" ... />


Running this code results on the following error:

SystemError: 'Provide value on 'System.Windows.Data.Binding' threw an exception.' Line number '12' and line position '7'.


The problem is that the SelectedValue (and SelectedItem) property is read-only.

There are several ways to deal with this problem. One alternative is to use a technique similar to the one described in the "Forwarding the Result of WPF Validation in MVVM" post. We're going to define an attached property which works as an "output only" property that could be used with data binding.

Attached property definition



I couldn't find a way to wrote the definition of the attached property in IronPython because it needed to be instanciated by XamlReader. So the definition was written using C#:

using System.Windows.Markup;
using System.Windows;
using System.Windows.Controls;
using System.IO;
using System.Collections.ObjectModel;


namespace Langexplr
{
public static class TreeViewSelectedBehavior
{
public static readonly DependencyProperty MySelectedProperty =
System.Windows.DependencyProperty.RegisterAttached(
"MySelected",
typeof(object),
typeof(TreeViewSelectedBehavior)
);
public static object GetMySelected(TreeView t)
{
return t.GetValue(MySelectedProperty);
}

public static void SetMySelected(TreeView t, object theValue)
{
t.SetValue(MySelectedProperty, theValue);
}


public static readonly DependencyProperty SelectedHelperProperty =
System.Windows.DependencyProperty.RegisterAttached(
"SelectedHelper",
typeof(TreeViewSelectedHelper),
typeof(TreeViewSelectedBehavior),
new UIPropertyMetadata(null,OnSelectedHelperChanged)
);

public static TreeViewSelectedHelper GetSelectedHelper(TreeView t)
{
return (TreeViewSelectedHelper)t.GetValue(SelectedHelperProperty);
}

public static void SetSelectedHelper(TreeView t,
TreeViewSelectedHelper theValue)
{
t.SetValue(SelectedHelperProperty, theValue);
}
static void OnSelectedHelperChanged(
DependencyObject depObj,
DependencyPropertyChangedEventArgs e)
{
((TreeViewSelectedHelper)e.NewValue).Register((TreeView)depObj);
}

}


This class define two properties:
  • MySelected: the output property that is used to set the selected element in the view model
  • SelectedHelper: which is used to as an object that modifies the value of MySelected when the selected item changes(see below).


The following helper class is used to subscribe the SelectedItemChanged event and change "MySelected" .

public class TreeViewSelectedHelper 
{
public TreeViewSelectedHelper() { }
void SelectedItemChanged(object sender,
RoutedPropertyChangedEventArgs<object> args)
{
(sender as TreeView).SetValue(
TreeViewSelectedBehavior.MySelectedProperty,
((sender as TreeView)).SelectedItem);
}
public void Register(TreeView t)
{
t.SelectedItemChanged += SelectedItemChanged;
}
}

}


We can now compile this class:


set NETFX4=c:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\
set WPFPATH=%NETFX4%\WPF
csc /debug /r:%NETFX4%System.Xaml.dll /r:%WPFPATH%\WindowsBase.dll /r:%WPFPATH%\PresentationCore.dll /r:%WPFPATH%\PresentationFramework.dll /target:library utils.cs


The example



Having defined this attached property and helper class we can now write the following example.

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:utils="clr-namespace:Langexplr;assembly=utils"
Title="TreeView selection test" Width="300" Height="300">
<Window.Resources>
<utils:TreeViewSelectedHelper x:Key="selHelper" />
</Window.Resources>

<StackPanel>

<TextBlock Text="{Binding selected.label}"/>
<TreeView ItemsSource="{Binding roots}"
utils:TreeViewSelectedBehavior.SelectedHelper="{StaticResource selHelper}">
<utils:TreeViewSelectedBehavior.MySelected>
<Binding Path="selected" Mode="OneWayToSource"/>
</utils:TreeViewSelectedBehavior.MySelected>

<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding children}">
<TextBlock Text="{Binding label}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
</Window>


With this XAML definition we can write the following IronPython code:

import clr
clr.AddReference("PresentationCore")
clr.AddReference("PresentationFramework")
clr.AddReference("WindowsBase")
clr.AddReference('GalaSoft.MvvmLight.WPF4.dll')


from System.Windows.Markup import XamlReader
from System.Windows import Application
from System.IO import File
from System.Windows.Controls import TreeView
import System
import clrtype
from GalaSoft.MvvmLight import ViewModelBase
from System.Collections.ObjectModel import ObservableCollection


class TestModel(ViewModelBase):
__metaclass__ = clrtype.ClrClass
def __init__(self):
self.name = 'algo'
self.root = NodeModel('x1',[NodeModel('y2',[NodeModel('z2',[])]),
NodeModel('y3',[])])

self.sselected = NodeModel('',[])

@property
@clrtype.accepts()
@clrtype.returns(System.Object)
def selected(self):
result = self.sselected
return result

@selected.setter
@clrtype.accepts(System.Object)
@clrtype.returns()
def selected(self, value):
self.sselected = value
self.RaisePropertyChanged('selected')

@property
@clrtype.accepts()
@clrtype.returns(System.Object)
def roots(self):
return [self.root]

@property
@clrtype.accepts()
@clrtype.returns(System.String)
def label(self):
return self.name

@label.setter
@clrtype.accepts(System.String)
@clrtype.returns()
def label(self, value):
self.name = value
self.RaisePropertyChanged('label')



class NodeModel:
__metaclass__ = clrtype.ClrClass

def __init__(self,label, initchildren):
self.children_collection = ObservableCollection[System.Object](initchildren)
self.name = label


@property
@clrtype.accepts()
@clrtype.returns(System.Object)
def label(self):
return self.name

@property
@clrtype.accepts()
@clrtype.returns(System.Object)
def children(self):
return self.children_collection


xamlFile = File.OpenRead('test.xaml')
window = XamlReader.Load(xamlFile)
window.DataContext = TestModel()
xamlFile.Close()

Application().Run(window)


By running this example we can see how the label of the selected element of the TreeView is reflected in the TextBlock defined above.