Showing posts with label wpf. Show all posts
Showing posts with label wpf. Show all posts

Sunday, November 13, 2011

Sharing .xaml in WPF and Silverlight

Sharing code between WPF and Silverlight is not difficult. Good how-to is in the Prism guide. However sharing more complex .xaml is not so easy. You have to handle different namespaces and properties.

Handling different namespaces

Silverlight toolkit components are accessible via special namespace. So if you want to use a WrapPanel in Silverlight, you write something like this:

<UserControl x:Class="UnifiedXaml.TestControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit">
    <toolkit:WrapPanel></toolkit:WrapPanel>
</UserControl>

WPF world is easier:

<UserControl x:Class="UnifiedXaml.TestControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <WrapPanel></WrapPanel>
</UserControl>

It's clear those two .xaml files are so similar that they should be just one file. Here is one tiny line that makes it possible:

namespace UnifiedXaml
{
    public class MyWrapPanel: WrapPanel { }
}

From now on, you can share the .xaml file:

<UserControl x:Class="UnifiedXaml.TestControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:ux="clr-namespace:UnifiedXaml">
    <ux:MyWrapPanel></ux:MyWrapPanel>
</UserControl>

Handling different properties

When you follow Prism guidelines, you have separate projects for WPF and Silverlight parts. That means the WPF/SL application resources dictionaries are also separate. So the easiest way to handle different properties is to use styles. Shared .xaml file specifies just the control and its style. All necessary properties for WPF are defined in the style stored in the WPF application resources dictionary and the same is done for Silverlight.

Wednesday, May 12, 2010

Silverlight validation with IronPython

Validation support in Silverlight is done via Visual State Manager. All invalid fields have red rectangle around themselves. Unfortunately, this does not work out of the box in IronPython. We have to push it a little bit.

To demonstrate how, I have created a small example. Create a Silverlight app template and change app.py and app.xaml:

C:\IronPython-2.6.1\Silverlight\script\sl.bat python validation
app.py
import clrtype
import pyevent
from System.Windows import Application
from System.Windows.Controls import UserControl
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs

class ValidationClass(INotifyPropertyChanged):
    __metaclass__ = clrtype.ClrClass
    PropertyChanged = None

    def __init__(self, win):
        self.win = win
        self._text = 'text'
        self.PropertyChanged, self._propertyChangedCaller = pyevent.make_event()

    def add_PropertyChanged(self, value):
        self.PropertyChanged += value

    def remove_PropertyChanged(self, value):
        self.PropertyChanged -= value

    def OnPropertyChanged(self, propertyName):
        if self.PropertyChanged is not None:
            self._propertyChangedCaller(self, PropertyChangedEventArgs(propertyName))

    @property
    @clrtype.accepts()
    @clrtype.returns(str)
    def text(self):
        return self._text

    @text.setter
    @clrtype.accepts(str)
    @clrtype.returns()
    def text(self, value):
        if not value.startswith('text'):
            raise Exception('Value must start with text!')
        self._text = value
        self.OnPropertyChanged('text')

class App:
    def __init__(self):
        self.root = Application.Current.LoadRootVisual(UserControl(), "app.xaml")
        self.root.DataContext = ValidationClass(self.root)

App()
app.xaml
<UserControl x:Class="System.Windows.Controls.UserControl"
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <StackPanel>
    <TextBox x:Name="tbValidate1" Width="100" Height="25" 
      Text="{Binding text, Mode=TwoWay, ValidatesOnExceptions=True,
      NotifyOnValidationError=True}" />
    <TextBox Width="100" Height="25" />
    <TextBlock Text="{Binding text}" HorizontalAlignment="Center" />
  </StackPanel>
</UserControl>

When you run this application (C:\IronPython-2.6.1\Silverlight\script\server.bat /d:validation), you'll find out the validation does not work. There is no red rectangle when you enter wrong value; e.g. wrong.

Note the second empty TextBox is there so you can move focus out of the first one to update bound property.

For whatever reason, the invalid component is not switched into invalid state. Could be IronPython bug, could be something else. Anyway to fix it, you have to switch the control into invalid state manually. Add the BindingValidationError event:

from System.Windows import VisualStateManager
from System.Windows.Controls import ValidationErrorEventAction

...

class App:
    def __init__(self):
        self.root = Application.Current.LoadRootVisual(UserControl(), "app.xaml")
        self.root.DataContext = ValidationClass(self.root)
        self.root.BindingValidationError += self.OnBindingValidationError

    def OnBindingValidationError(self, sender, event):
        if event.Action == ValidationErrorEventAction.Added:
            VisualStateManager.GoToState(event.OriginalSource, 'InvalidUnfocused', True)
        else:
            VisualStateManager.GoToState(event.OriginalSource, 'Valid', True

Now when you enter wrong value into TextBox, you can see red rectangle around the control. You also see, the bound variable has the old, correct value text:

You can download the whole source here.

Tuesday, March 16, 2010

Parsing XML with XDocument

I needed to parse a XML document recently in Silverlight. Unfortunately, Silverlight does not have System.Xml.XmlDocument type so you need to use System.Xml.Linq.XDocument.

The following example works in Silverlight and with small change also in WPF.

# encoding: utf-8
import clr
clr.AddReferenceToFile('System.Xml.Linq.dll')
from System.Xml.Linq import XDocument, XNamespace

content = """<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss"
    xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <item>
            <title>This is the title</title>
            <media:description type="html"><p></p></media:description>
            <link>html/dsc00001.html</link>
            <media:thumbnail url="preview/dsc00001.jpg"/>
            <media:content url="web/dsc00001.jpg"/>
        </item>
        <item>
            <title></title>
            <media:description type="html"><p></p></media:description>
            <link>html/dsc00002.html</link>
            <media:thumbnail url="preview/dsc00002.jpg"/>
            <media:content url="web/dsc00002.jpg"/>
        </item>
    </channel>
</rss>"""

xDoc = XDocument().Parse(content)
namespace = XNamespace.Get("http://search.yahoo.com/mrss")
for item in xDoc.Element('rss').Element('channel').Elements('item'):
    print item.Element('title').Value
    print item.Element(namespace+'thumbnail').Attribute('url').Value
Here is the output:
This is the title
preview/dsc00001.jpg

preview/dsc00002.jpg

You have to have System.Xml.Linq.dll from Silverlight SDK next to your app.py.

The change for WPF:

clr.AddReference('System.Xml.Linq')

Also make sure you don't have Silverlight's System.Xml.Linq.dll next to your script.

Wednesday, November 11, 2009

INotifyPropertyChanged and databinding in IronPython WPF

INotifyPropertyChanged is important interface for building WPF or Silverlight applications using M-V-VM concept (MSDN article).

In simple language, you have a Model which provides access to your data (e.g in database, files, web, etc.). Then you have a ViewModel that access data in the Model via Model's interface and provides data to a View which is XAML file with UI layout. Linkage between ViewModel and View is done by binding that utilizes PropertyChanged event to properly update all UI elements.

I have found two examples how to implement INotifyPropertyChanged interface in IronPython. The first one uses __setattr__ hook. Personally, I don't like it - it is not clear and easily readable code. The second one is better because it uses properties. But you have to write self.OnPropertyChanged("my_property_name") for every property. Not ideal.

That's why I sit down and write a notify_property:

class notify_property(property):

    def __init__(self, getter):
        def newgetter(slf):
            try:
                return getter(slf)
            except AttributeError:
                return None
        super(notify_property, self).__init__(newgetter)

    def setter(self, setter):
        def newsetter(slf, newvalue):
            oldvalue = self.fget(slf)
            if oldvalue != newvalue:
                setter(slf, newvalue)
                slf.OnPropertyChanged(setter.__name__)
        return property(
            fget=self.fget,
            fset=newsetter,
            fdel=self.fdel,
            doc=self.__doc__)

With this subclass I aimed several goals:

  • usage simple as @property decorator (actualy no other usage is possible as I implemented __init__ with just one parameter that must be the getter)
  • when property is on yet defined, it should return None
  • automaticaly handle PropertyChanged event when and only when property has changed

We also need to implement INotifyPropertyChanged interface in IronPython so we can call OnPropertyChanged method. See Overiding events in IronPython\Doc\dotnet-integration.rst to understand what means add_ and remove_ methods.

class NotifyPropertyChangedBase(INotifyPropertyChanged):
    PropertyChanged = None

    def __init__(self):
        self.PropertyChanged, self._propertyChangedCaller = pyevent.make_event()

    def add_PropertyChanged(self, value):
        self.PropertyChanged += value

    def remove_PropertyChanged(self, value):
        self.PropertyChanged -= value

    def OnPropertyChanged(self, propertyName):
        if self.PropertyChanged is not None:
            self._propertyChangedCaller(self, PropertyChangedEventArgs(propertyName))

Now we can implement a simple class with properties with change notification:

class DataObject(NotifyPropertyChangedBase):
    
    def __init__(self, size):
        super(DataObject, self).__init__()
        self.size = size

    @notify_property
    def size(self):
        return self._size

    @size.setter
    def size(self, value):
        self._size = value

You can see it is very easy - just like any other property in Python.

Finaly, let's put all together. When you run the code below, it shows a window with label, textbox and button. The label is updated as you type into the textbox and a message is written into the console as well. By default, the textbox is updated when it looses focus, so I have to change UpdateSourceTrigger to PropertyChanged. When you click the button, the value is reset. Note if you use type int instead of string the two-way bindign would not work.

notpropwpf.py

import clr
import System
clr.AddReference('PresentationFramework')
clr.AddReference('PresentationCore')

from System.Windows.Markup import XamlReader
from System.Windows import Application, Window
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs
import pyevent

XAML_str = """<window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="250" Height="62">
    <stackpanel x:Name="DataPanel" Orientation="Horizontal">
        <label Content="Size"/>
        <label Content="{Binding size}"/>
        <textbox x:Name="tbSize" Text="{Binding size, UpdateSourceTrigger=PropertyChanged}" />
        <button x:Name="Button" Content="Set Initial Value"></Button>
    </StackPanel>
</Window>"""

class notify_property(property):

    def __init__(self, getter):
        def newgetter(slf):
            #return None when the property does not exist yet
            try:
                return getter(slf)
            except AttributeError:
                return None
        super(notify_property, self).__init__(newgetter)

    def setter(self, setter):
        def newsetter(slf, newvalue):
            # do not change value if the new value is the same
            # trigger PropertyChanged event when value changes
            oldvalue = self.fget(slf)
            if oldvalue != newvalue:
                setter(slf, newvalue)
                slf.OnPropertyChanged(setter.__name__)
        return property(
            fget=self.fget,
            fset=newsetter,
            fdel=self.fdel,
            doc=self.__doc__)

class NotifyPropertyChangedBase(INotifyPropertyChanged):
    PropertyChanged = None

    def __init__(self):
        self.PropertyChanged, self._propertyChangedCaller = pyevent.make_event()

    def add_PropertyChanged(self, value):
        self.PropertyChanged += value

    def remove_PropertyChanged(self, value):
        self.PropertyChanged -= value

    def OnPropertyChanged(self, propertyName):
        if self.PropertyChanged is not None:
            self._propertyChangedCaller(self, PropertyChangedEventArgs(propertyName))

class ViewModel(NotifyPropertyChangedBase):
    
    def __init__(self):
        super(ViewModel, self).__init__()
        # must be string to two-way binding work correctly
        self.size = '10'

    @notify_property
    def size(self):
        return self._size

    @size.setter
    def size(self, value):
        self._size = value
        print 'Size changed to %r' % self.size

class TestWPF(object):

    def __init__(self):
        self._vm = ViewModel()
        self.root = XamlReader.Parse(XAML_str)
        self.DataPanel.DataContext = self._vm
        self.Button.Click += self.OnClick
        
    def OnClick(self, sender, event):
        # must be string to two-way binding work correctly
        self._vm.size = '10'

    def __getattr__(self, name):
        # provides easy access to XAML elements (e.g. self.Button)
        return self.root.FindName(name)

tw = TestWPF()
app = Application()
app.Run(tw.root)

You need pyevent.py from IronPython\Tutorial\ folder to run to example.

Unfortunately, this does not work in Silverlight, probably because the property is not .NET field. See next atricle for Silverlight version.