|
Design-time data inside Expression Blend (for WPF Apps) Introduction: Data templates enable ‘automatic’ generation of user interface
to represent a domain object. Figure 1 – Given a domain
object (Car) and a data template, WPF’s data binding engine magically produces
user interface. As you can see from Figure 1, Data Templating is a very
powerful feature for UI designers because it allows them to create the visual
representation of a domain or business object. The designer uses Blend as a
WYSIWYG editor for the template; the developer uses code to create the object's
definition (a.k.a the class) and WPF magically marries the two together at
run-time when the template is needed. 1. All visuals are created and packaged in the context of a template (a transparent process to a designer). 2. In order to visually create the templates, designers often need sample data (which in this paper I call 'design-time data'. Blend has support for this design-time data but there is no
specific guidance or best practice on how to create it and integrate it into an
application. This writing aims to share my lessons learned and personal
preferences around creating and using this design-time data. Expression Blend out-of -box experience.As you can see from the Blend User Interface in Figure 2, it supports two types of datasources: XML and a “CLR object”.
Figure 2 – Blend’s
default workspace, plus an ugly red arrow! XML support.If you click on XML, Blend will prompt you for a URL to an XML file. Blend will read the file and infer a schema to represent the data you are trying to describe. Since this approach requires no code, it is probably the easiest entry point for designers to generate design-time data. However, there are a few down-sides to using XML to represent your design-time data: · The syntax is different from the syntax used for binding to CLR objects and most projects do not directly bind to XML, so it is likely that the syntax needs to be replaced for the data templates to work in the real code; performing the replacement is not a big deal (other than the iterative nature of design work might have you repeating the task). · XML representation of complex data is not equivalent to object representation of same data. Typical inference rules in XML translate an XML Element to an object and an XML attribute to a property; there is no set rule inferring properties that are themselves complex objects. Because of these two reasons, I almost never use XML data sources; the only time I use XML for design-time data is on very early prototypes where the domain objects have not been created. Once these are created, it is not hard to use domain objects and be much closer to the final code. CLR object supportWPF and Silverlight can data bind to any .NET object with
public properties and you should be able to create a template for any such
object. Unfortunately, the "Add CLR object Data Source" dialog in Blend
Figure 3 - Blend's "Add CLR Object Data Source" dialog. This is the typical way to add data sources to a project. This dialog only shows the classes that have a public constructor with no parameters.
Getting around Blend's discovery constraints.As mentioned above, Blend has some small limitations when discovering object data sources. These limitations are not in the platform (WPF) but in the tool. You can get around these constraints by typing XAML manually into Blend; if you do type XAML manually, Blend will respect the data source declaration and allow you to design data using data source. Here is the syntax for the most common scenarios not supported out of the box: Creating a design-time datasource from a class with parameters in the constructor:
Creating a data source from an instance of a class:
|
|||||||||||||||||||
|
<ObjectDataProvider
x:Key="CarDS"
ObjectInstance="{x:Static XModel:DesignTimeData.SingleCar}" ObjectType="{x:Type XModel:Car}" /> |
public class DesignTimeData { public static Car
SingleCar { get {} }… |
|
From XAML you just bind with out a Path <Grid x:Name="LayoutRoot"
DataContext="{Binding
Mode=Default, Source={StaticResource DesignTimeDataDS}}"> |
|
Design-time
data that does not interfere with run-time.
So far, the data sources we have been using are literally inserted into the
scene and code is being executed at run-time; there is no way to flag them as
design-time only data sources. Blend does have design-time flags for other
features (like layout & window size), but nothing for data (yet!).
There are several techniques to ‘reset’ the datasources at run-time:
·
You can remove the ObjectDataProvider before
shipping the code. This is easy for
smaller projects where you have a few data sources to comment out; as projects
get larger, this is more work. Still quite acceptable.
Tip: When ‘removing’ it, I like to simply comment it out, so you can uncomment
it later if needed and use it again.
· You can write a few lines of code on your scene to reset bindings at run-time. The pattern here tends to be to reset the data context of any UIElement that was databound to design-time data. This is usually done in the constructor for your scene, after InitializeComponent().
|
public Window1() { this.InitializeComponent(); // this
removes the design-time data context, this.LayoutRoot.DataContext
= null; /* or you can
just override with the right run-time value*/ this.LayoutRoot.DataContext
= GetRuntimeData(); |
The ‘down-side’ to this pattern is that you are ‘resetting’ the design-time
work but still letting it run (which is consuming a few cycles and doing work,
as little as that work might be). You
are also exposed to making a mistake and forgetting to reset a binding.
I think of both options above as band-aids (which again work well for smaller projects); to truly create design-time data that does not interfere at run-time, we need to write a bit more code.
· You can create design-time data that only works at design-time. A sample code for this scenario looks like this:
|
public static class DesignTimeData { public
static ObservableCollection<Car> Lot { get { #if DEBUG if
( !((bool)DependencyPropertyDescriptor.FromProperty(DesignerProperties.IsInDesignModeProperty, typeof(DependencyObject)).Metadata.DefaultValue )) return
new ObservableCollection<Car>(); return
Car.Lot; #else return new
ObservableCollection<Car>(); ; #endif } } … } |
You will notice:
· I like static classes for design-time data. For practical purposes, static class= abstract and sealed so it helps me prevent from making a mistake and using the class or inheriting from it.
· I like static properties for design-time data. This is not out of the box supported syntax in Blend, but I like the syntax best because:
o It minimizes the total number of classes I have to create on my test/design-time object model. I can just attach many static properties to a single class.
o It keeps the syntax clean in the XAML (none of my XAML bindings have Paths, I bind to data sources directly).
o This forces me to create a data source (or ObjectDataProvider) for each DataContext I intend to use. The default blend model allows me to add a data source and then share it by using property paths, this can cause problems when I need to delete it; there might be more than one Binding referring to it.
· I have a big #if DEBUG around the code. This way, no chance I miss it in release code.
· Inside my #if DEBUG code I still check if I am in Design-time. 90% of my development cycle is in DEBUG mode; I often ‘smoke test’ in DEBUG, so I need to make sure I am still running valid paths when in DEBUG mode.
·
A KNOWN
ISSUE: Notice that the property above does not return null. Instead it returns an Empty Collection. This
is because of a bug in the XAML parser, where you cannot use {x:Static SomeObject.Property} and have the value of
SomeObject.Property be null.
The parser will throw an exception. The bug has already been fixed on WPF4.
The above code checking for design-time and for DEBUG is almost where we need to be, but it has one last draw-back, wiring up the binding from XAML takes a real property (e.g. DataContext) and assigns to it; this means that same property can’t be used (from XAML) for the real value that it needs at run-time, how to fix this? Attached behaviors, of course!
An attached behavior leverages dependency properties to attach a property to an existing object.
The owner of the attached property will be notified via a callback when the property is set, and the callback gets passed the instance object that the property is being set on, so inside the callback, the attached behavior can perform any public operations on the object that the property is set on; in this case we are going to override the DataContext of that object.
Here is what the Attached behavior looks like:
|
public class DataContextSetterBehavior { public static object
GetDataContext(DependencyObject obj) { return
(object)obj.GetValue(DataContextProperty); } public static void
SetDataContext(DependencyObject obj, object value) { obj.SetValue(DataContextProperty,
value); } /// <summary> /// DataContext is an attached dependency
property; the value assigned to it is forwarded it to the DataContext
property of the actual FrameworkElement this property is being set on /// </summary> public static readonly DependencyProperty DataContextProperty = DependencyProperty.RegisterAttached("DataContext", typeof(object), typeof(DataContextSetterBehavior),
new UIPropertyMetadata(new PropertyChangedCallback(OnDataContextChanged))); /// <summary> /// This is the callback whenever the DataContextProperty is
set. /// </summary> /// <param
name="d">is the instance object
that the property is set on, this is where we will set the DataContext on</param> /// <param
name="e">includes the NewValue
being set, we will forward this to d's DataContext </param> static void OnDataContextChanged(DependencyObject
d, DependencyPropertyChangedEventArgs e) { if
(!System.ComponentModel.DesignerProperties.GetIsInDesignMode(d)) { // We
are not in design mode, ignore the setter return; } FrameworkElement
fe = d as FrameworkElement; if
(fe != null) { fe.DataContext = e.NewValue; } else { FrameworkContentElement
fce = d as FrameworkContentElement; if
(fce != null) fce.DataContext = e.NewValue; } } } |
The behavior is reusable. You can use it for any design-time-data you want.
Here is how you use the behavior from XAML
|
<Grid x:Name="LayoutRoot" local:DataContextSetterBehavior.DataContext="{x:Static XModel:DesignTimeData.Lot}" > |
In this case, I am referring to the same static we used earlier for design-time
data, and I added code in the window class to initialize the DataContext for
the window, and have it inherit everywhere within the window.
|
public partial class AttachedBehaviorSampleWindow
: Window { public
AttachedBehaviorSampleWindow() { this.DataContext
= Car.Lot; this.InitializeComponent(); } } |
Summary:
Data Templates and Data binding are two very powerful features in WPF; they
empower the designer to create a great looking, high-fidelity user interface
without needing to understand code, all they need is design-time data. Blend 2 has limited support for design-time
data (using ObjectDataProvider); for more complicated data, writing a two liner on XAML gives you options
to call constructors w/ parameters, or to refer to static objects; you can go
as far as you need to, with the most elegant solution being an attached
behavior that forwards DataContext to an object at design-time, but not at
run-time.
All the stuff I skipped:
I wrote this article six months ago and all I left empty was the summary
and this “skipped” section. I no longer
recall what I was going to put in here, but I am sure it needed to be here, so
I am leaving this section open in case I invent a time machine and go back in
time to remember (or in case someone emails a question that I can’t answer).