Sonntag, 20. Juni 2010

Silverlight: Managing presentation model state

In our current project Privatbilanz we had the problem that we have to manage the state of the IsExpanded property in a treeview.

Our architecture:
  • Data Access Layer: Entity Framework
  • WCF RIA Services
  • Silverlight client

After each manipulation of our nodes of the treeview, we have to call the WCF service to give us back the fresh list of nodes. After this action the tree will have has initial look – all nodes are collapsed :-(

So what we did now was to extend our data model at the client side with a partial class to define for our entity a IsExpanded property.

Step 1 – Extend entity
   1: public partial class AnzeigeKategorie
   2:     {
   3:         private bool _isExpanded;
   4:         
   5:         public bool IsExpanded
   6:         {
   7:             get { return _isExpanded; }
   8:             set
   9:             {
  10:                 if (value == _isExpanded) return;
  11:                 _isExpanded = value;
  12:                 RaisePropertyChanged("IsExpanded");
  13:             }
  14:         }
  15:     }

Note: You have to use the same namepace as your model at the service side (WCF RIA Services)

Step 2 – Bind the property to UI
   1: <!--TreeViewItem Style-->
   2:     <Style TargetType="controls:TreeViewItem">
   3:         <Setter Property="Padding" Value="3"/>
   4:         <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
   5:         <Setter Property="VerticalContentAlignment" Value="Top"/>
   6:         <Setter Property="Background" Value="Transparent"/>
   7:         <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
   8:         <Setter Property="FontFamily" Value="{StaticResource ContentFontFamily}"/>
   9:         <Setter Property="FontSize" Value="{StaticResource ContentFontSize}"/>
  10:         <Setter Property="TextOptions.TextHintingMode" Value="Animated"/>
  11:         <Setter Property="BorderThickness" Value="1"/>
  12:         <Setter Property="Cursor" Value="Arrow"/>
  13:         <Setter Property="IsTabStop" Value="True"/>
  14:         <Setter Property="TabNavigation" Value="Once"/>
  15:         <Setter Property="Margin" Value="0 1 0 0"/>
  16:         <Setter Property="Helpers:SetterValueBindingHelper.PropertyBinding">
  17:             <Setter.Value>
  18:                 <Helpers:SetterValueBindingHelper Property="IsExpanded" Binding="{Binding IsExpanded, Mode=TwoWay}" />
  19:             </Setter.Value>
  20:         </Setter>

Because in our case the control is a TreeviewItem we have to do this in our style definition. Unfortunately Silverlight does not support this kind of binding at the normal way – WPF is at this point much better.

So what we have to do additional is to define a helper class SetterValueBindingHelper. We found this really useful class at Delay’ Blog:

As the platform evolves, so do the workarounds [Better SetterValueBindingHelper makes Silverlight Setters better-er!] - Delay's Blog - Site Home - MSDN Blogs

 
Step 3 – Helper class for Setter binding
   1: /// <summary>
   2:     /// Class that implements a workaround for a Silverlight XAML parser
   3:     /// limitation that prevents the following syntax from working:
   4:     ///    &lt;Setter Property="IsSelected" Value="{Binding IsSelected}"/&gt;
   5:     /// </summary>
   6:     public class SetterValueBindingHelper
   7:     {
   8:         /// <summary>
   9:         /// Optional type parameter used to specify the type of an attached
  10:         /// DependencyProperty as an assembly-qualified name, full name, or
  11:         /// short name.
  12:         /// </summary>
  13:         [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods",
  14:             Justification = "Unambiguous in XAML.")]
  15:         public string Type { get; set; }
  16:  
  17:         /// <summary>
  18:         /// Property name for the normal/attached DependencyProperty on which
  19:         /// to set the Binding.
  20:         /// </summary>
  21:         public string Property { get; set; }
  22:  
  23:         /// <summary>
  24:         /// Binding to set on the specified property.
  25:         /// </summary>
  26:         public Binding Binding { get; set; }
  27:  
  28:         /// <summary>
  29:         /// Gets the value of the PropertyBinding attached DependencyProperty.
  30:         /// </summary>
  31:         /// <param name="element">Element for which to get the property.</param>
  32:         /// <returns>Value of PropertyBinding attached DependencyProperty.</returns>
  33:         [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
  34:             Justification = "SetBinding is only available on FrameworkElement.")]
  35:         public static SetterValueBindingHelper GetPropertyBinding(FrameworkElement element)
  36:         {
  37:             if (null == element)
  38:             {
  39:                 throw new ArgumentNullException("element");
  40:             }
  41:             return (SetterValueBindingHelper)element.GetValue(PropertyBindingProperty);
  42:         }
  43:  
  44:         /// <summary>
  45:         /// Sets the value of the PropertyBinding attached DependencyProperty.
  46:         /// </summary>
  47:         /// <param name="element">Element on which to set the property.</param>
  48:         /// <param name="value">Value forPropertyBinding attached DependencyProperty.</param>
  49:         [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
  50:             Justification = "SetBinding is only available on FrameworkElement.")]
  51:         public static void SetPropertyBinding(FrameworkElement element, SetterValueBindingHelper value)
  52:         {
  53:             if (null == element)
  54:             {
  55:                 throw new ArgumentNullException("element");
  56:             }
  57:             element.SetValue(PropertyBindingProperty, value);
  58:         }
  59:  
  60:         /// <summary>
  61:         /// PropertyBinding attached DependencyProperty.
  62:         /// </summary>
  63:         public static readonly DependencyProperty PropertyBindingProperty =
  64:             DependencyProperty.RegisterAttached(
  65:                 "PropertyBinding",
  66:                 typeof(SetterValueBindingHelper),
  67:                 typeof(SetterValueBindingHelper),
  68:                 new PropertyMetadata(null, OnPropertyBindingPropertyChanged));
  69:  
  70:         /// <summary>
  71:         /// Change handler for the PropertyBinding attached DependencyProperty.
  72:         /// </summary>
  73:         /// <param name="d">Object on which the property was changed.</param>
  74:         /// <param name="e">Property change arguments.</param>
  75:         private static void OnPropertyBindingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  76:         {
  77:             // Get/validate parameters
  78:             var element = (FrameworkElement)d;
  79:             var item = (SetterValueBindingHelper)(e.NewValue);
  80:             if ((null == item.Property) || (null == item.Binding))
  81:             {
  82:                 throw new ArgumentException(
  83:                     "SetterValueBindingHelper's Property and Binding must both be set to non-null values.");
  84:             }
  85:  
  86:             // Get the type on which to set the Binding
  87:             Type type = null;
  88:             if (null == item.Type)
  89:             {
  90:                 // No type specified; setting for the specified element
  91:                 type = element.GetType();
  92:             }
  93:             else
  94:             {
  95:                 // Try to get the type from the type system
  96:                 type = System.Type.GetType(item.Type);
  97:                 if (null == type)
  98:                 {
  99:                     // Search for the type in the list of assemblies
 100:                     foreach (var assembly in AssembliesToSearch)
 101:                     {
 102:                         // Match on short or full name
 103:                         type = assembly.GetTypes()
 104:                             .Where(t => (t.FullName == item.Type) || (t.Name == item.Type))
 105:                             .FirstOrDefault();
 106:                         if (null != type)
 107:                         {
 108:                             // Found; done searching
 109:                             break;
 110:                         }
 111:                     }
 112:                     if (null == type)
 113:                     {
 114:                         // Unable to find the requested type anywhere
 115:                         throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
 116:                             "Unable to access type \"{0}\". Try using an assembly qualified type name.",
 117:                             item.Type));
 118:                     }
 119:                 }
 120:             }
 121:  
 122:             // Get the DependencyProperty for which to set the Binding
 123:             DependencyProperty property = null;
 124:             var field = type.GetField(item.Property + "Property",
 125:                 BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Static);
 126:             if (null != field)
 127:             {
 128:                 property = field.GetValue(null) as DependencyProperty;
 129:             }
 130:             if (null == property)
 131:             {
 132:                 // Unable to find the requested property
 133:                 throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
 134:                     "Unable to access DependencyProperty \"{0}\" on type \"{1}\".",
 135:                     item.Property, type.Name));
 136:             }
 137:  
 138:             // Set the specified Binding on the specified property
 139:             element.SetBinding(property, item.Binding);
 140:         }
 141:  
 142:         /// <summary>
 143:         /// Returns a stream of assemblies to search for the provided type name.
 144:         /// </summary>
 145:         private static IEnumerable<Assembly> AssembliesToSearch
 146:         {
 147:             get
 148:             {
 149:                 // Start with the System.Windows assembly (home of all core controls)
 150:                 yield return typeof(Control).Assembly;
 151:  
 152:                 // Fall back by trying each of the assemblies in the Deployment's Parts list
 153:                 foreach (var part in Deployment.Current.Parts)
 154:                 {
 155:                     var streamResourceInfo = Application.GetResourceStream(
 156:                         new Uri(part.Source, UriKind.Relative));
 157:                     using (var stream = streamResourceInfo.Stream)
 158:                     {
 159:                         yield return part.Load(stream);
 160:                     }
 161:                 }
 162:             }
 163:         }
 164:     }