Wednesday, October 01, 2008
#
My blog has moved to team.interknowlogy.com/blogs/johnbowen/
Check it out for recent posts!
Sunday, June 29, 2008
#
Thanks to everyone who attended my sessions!
Application Layout in XAML:
Handout | Code
Also look at this previous post for more on sharing sizing between Grids.
We didn't get to cover the FrameworkElement layout properties so check out the second application in the sample code to get a feel for how they work.
Building Controls for WPF:
Slides | Code
You can also download the full class library reference poster here.
WPF Data Binding:
Slides | Code
Also look at my other blog posts for more examples and explanations.
Creating Responsive Desktop Apps:
Code
Friday, May 30, 2008
#
The XAML Resources in WPF and Silverlight can be a powerful tool but are often misunderstood and can cause lots of problems if not managed properly. The first point of confusion with Resources is that the term "resources" already has so many other meanings, including:
- Project items compiled as Resource
- Linked and embedded resources (resx files)
- Hardware resources, and specifically graphics in WPF
So first to clarify what we're talking about here: XAML Resources are any .NET object that is declared in a ResourceDictionary. This is generally done in XAML but objects can also be added to a ResourceDictionary in code. ResourceDictionaries can be found on any FrameworkElement derived object (and also on Application) in the form of the Resources DependencyProperty.
Resources are typically accessed with the StaticResource or DynamicResource MarkupExtensions in XAML. I won't get too much into the details of these except for a few key points. DynamicResource will update if the referenced Resource changes which can be useful but generally just causes extra overhead that can create performance and memory problems. StaticResource requires that the referenced Resource exist in the resource tree at load time. More on that later. StaticResource can cause compile-time errors for missing Resources while Dynamic will just use a default value until the Resource becomes available. StaticResource should always be your default choice unless you have a specific need for the behavior of Dynamic (Blend always uses Dynamic so be prepared to make lots of switches in what it generates).
Next, what is the Resource tree? WPF UI is inherently hierarchical. Each UI is made of a logical tree that is then used to render a visual tree. This means that each element in your UI has a clear parent structure with a single path up the hierarchy to the root Window element (or NavigationWindow, Page…) and then an Application object at the very top. The Resources property of each parent element is made available to every child element so that elements can access any Resource that is declared anywhere in its parent hierarchy. This makes it possible to share Resources between elements in specific areas while not requiring the memory and performance penalty of declaring them application-wide.
To get the maximum reuse benefits while not overly impacting performance, resources need to be carefully managed and organized. In addition to resources from the parent hierarchy, the MergedDictionary feature allows you to extract any Resource to a separate ResourceDictionary file that can then be imported to specific elements that need it. This can allow you to share XAML between elements at a lower level but it can also cause situations where multiple instances of a ResourceDictionary are created by elements under a common parent, consuming more memory and initialization time than if the ResourceDictionary were merged once into the parent.
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/Brushes.xaml"/>
<ResourceDictionary Source="/MyCompany.MyApplication;component/Resources/Styles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Once you've created and structured your Resources you can get to the important part: actually using them. The most compelling reason to use a Resource is reuse but even in cases where a resource is used once they can still provide value by simplifying your XAML's structure. Declaring things like templates and brushes as resources rather than inline can turn a deep hierarchy into a single line of XAML and can also introduce a descriptive name for a sub-section of XAML, making it more readable (it's no coincidence that this sounds like refactoring methods). There are also cases (ItemsControl ItemTemplates in particular) where declaring things in-line can actually break them but simply moving them to resources will fix the problem.
The primary method of accessing a Resource is by Key. Keys are specified by the x:Key attribute on the element that is declared as a Resource. Keys are a required attribute for most types of resources and will cause compiler errors if missing. Static and DynamicResource references rely on Keys to find resources.
The other method of accessing resources instead relies on a type that is associated with a Style (TargetType) or DataTemplate (DataType). When no x:Key is specified on one of these types they are assigned an implicit key that will cause them to be applied to any object of the specified type in whose Resource tree they exist. This can be very powerful but also cause problems that are very hard to locate. For example, if someone creates a Style with TargetType of Button that sets Width to 100 and puts it in App.xaml, suddenly all Buttons application-wide will be fixed at 100 unless they have some other explicit setting for Width. Now someone trying to figure out why their Button text is getting cropped needs to figure out where that fixed width is coming from but gets no clues from the settings on the Button itself. It's pretty easy to go check Window.Resources or App.xaml but now imagine the Style is located in one of 10 ResourceDictionary files that are merged into App.xaml. Implicit key resources should be used very carefully and be declared only in well known locations to avoid this sort of problem.
The first thing you should do when tracking down an issue with a named (explicit Key) Resource is to make sure the reference is using StaticResource rather than DynamicResource. This can often turn a mysterious run-time behavior into a compiler error, or in some cases a run-time exception, telling you immediately that the Resource doesn't exist in the current Resource tree. If this happens it can most often be fixed simply by merging in a ResourceDictionary or just moving the Resource declaration itself.
If you find yourself getting into problems with name collisions (i.e. 10 different people created "ButtonStyle1" in Blend and spread them all over the app) it can help to actually diagram out the resource tree for the element in question (on paper or in your head) to find out which one is really being used. It starts at the top with App.xaml, then the Window or Page, then local parent controls' resources, then the element itself. Inside each of these sections include any MergedDictionaries and anything in turn merged into those ResourceDictionaries. Once you can see the whole tree, whatever is declared last takes precedence: this goes for inside files as well as down the tree.

So if ButtonStyle1 is declared in Common.xaml (A) that is the first file merged into App.xaml but also as the last local Resource in App.xaml (B) and again in CustomStyles.xaml (C) merged into Window1.xaml then A and B are going to be ignored and C will be used. If you then rename C to ButtonStyle2, A will be ignored and B will be used. Using Visual Studio's Find in Files is usually the quickest way to find name collisions or just track down lost resources (use x:Key="MyResourceKey" to find declarations).
Thursday, May 22, 2008
#
The purpose of Silverlight 2 in demo applications has so far primarily been to
provide enhanced graphics to self contained applications. While the graphics
capabilities are attention-grabbing, some of the other powerful features that
are available seem to get overlooked. For example, Silverlight has the ability
to interact with the HTML DOM and Javascript which allows you to use it to
enhance existing web applications. To try out some of these features, I took a
basic data entry form (in this case just plain HTML that's not actually hooked
up to anything on the server) and added a toolbar that gives the user some
editing features to help fill out the form.
The toolbar provides
-
Unlimited Undo/Redo
-
Clearing of all form data
-
Saving multiple sets of the
entire contents of the form locally on the client
-
Reloading of any set of data
that is saved on the client
The only javascript that is required for this functionality is some boilerplate
to connect to the Silverlight C# code.
For the toolbar I used the DHTML-Silverlight connections in both directions. The
DataCache class in the application is decorated with the ScriptableType
attribute and has methods decorated with the ScriptableMember attribute. This is
the first step in making code available to javascript. The next is to call
HtmlPage.RegisterScriptableObject("objectName", theScriptableObjectInstance)
from the Silverlight UserControl's code-behind. A good place to do this is in a
handler for the Loaded event. These methods are now available to call from
javascript by getting a reference to the Silverlight application like this:
var agEControl = document.getElementById("SilverlightElementId");
var content = agEControl.content;
var scriptableObject = content.objectName;
Here objectName is the string used above to register the object and
"SilverlightElementId" is the HTML id of your Silverlight control ("Xaml1" by
default in a VS generated project). Once you have scriptableObject you can just
call any method on it just as you would in C#.
In the scriptable methods I use the connection in the other direction to go back
to the calling HTML and retrieve values from the page's controls. This basically
takes one step to get the current value of an element:
HtmlElement element = HtmlPage.Document.GetElementById("controlId");
string value = element.GetAttribute("value");
It looks just like what you would do in javascript with the exception of the
HtmlElement and HtmlPage classes that provide the access to the DOM. HtmlElement
allows you to do all sorts of manipulation including attaching event handlers
and playing with the structure of elements on the page.
Another interesting thing I ran into when I decided to blog this is the
simplicity of deployment with the current structure of a Silverlight
application. If you're working in Visual Studio your application is compiled
into a single file with a .xap extension. You probably also have a web project
of some type to host the application. When running your application the built-in
web server is spun up to serve up the page to the browser.
If you happen to be using a plain HTML host and not using server-side code you
can also open the .htm directly in your browser as a file. This gave me the idea
to try xcopy deploying my app to my blog's downloads folder, which is just a
basic file share. Starting out with a minimum set of files I copied the .htm and
.js files that make up my page (no generated files) and the .xap file in
ClientBin. This didn't work at first due I think to extension filtering that
disallowed the browser from pulling down the xap file. Fortunately zip files are
allowed to download and (big secret) xap files are actually just zip files
containing your assemblies and a manifest. If you rename your xap to zip you can
just open it up and see your dlls inside. Once I did this and adjusted the
reference in the HTML accordingly the entire page with toolbar loaded in my
browser over http from a simple file share! Compare this to what you need to
stand up a simple AJAX application and you can see the advantage.
Download the sample code
HERE.
Try out the functional page
HERE. You'll need the Silverlight 2 Beta 1 runtime (it should ask to install
itself). Sets of data are saved by Last Name and ID so you may notice the Save
button being enabled/disabled as you change fields. The Load and Delete buttons
will become enabled after you save a form. The set selector is kind of clunky
because there's no ComboBox control yet in Beta 1 so I just squeezed in a small
ListBox instead.
**UPDATE** - Silverlight 2 Beta 2 was just released and I've updated the functional version
and put it alongside the original
HERE. **
Sunday, May 11, 2008
#
Blend has been getting more stable with frequent releases including SP1, Blend 2
etc but it still can't render everything, even from an application that runs
flawlessly at runtime. There are lots of things that can break the Blend
renderer but tracking down exactly what broke your window/page/control can be
difficult and time consuming. In some cases the location can be found by taking
advantage of how Blend hosts applications. When Blend renders XAML it's running
the code just like at runtime and the code can be debugged in the same way. In
cases where non-XAML Exceptions are breaking the renderer Visual Studio can
attach to Blend itself and break to the exact location that's causing the
problem.
The primary reason errors occur in Blend that don't show up at runtime is that
Blend acts as a host for the application. This causes things that are evaluated
relative to the running application like config files and certain types of
relative resource references to look in the wrong place. The pack:application
syntax in particular causes invalid references. The ResourceDictionary hierarchy
is also different when running in Blend. In versions prior to Blend 2 Feb Beta
App.xaml resources aren't automatically resolved using StaticResource the way
they are at compile time. Even the newest versions run into problems resolving
references inside things like UserControls when rendered as part of a Window.
XAML errors generally show up in the Blend errors window and can be linked to a
specific line (with varying accuracy depending on the version) while errors
deeper in code need Visual Studio debugging.
To debug an application inside Blend open the project in both Visual Studio and
Blend at the same time. Before opening the broken component in Blend attach
Visual Studio to the process with Debug->Attach To Process and select Blend.exe
from the list. To make sure the Exception isn't just swallowed by Blend set
Visual Studio to break on all CLR Exceptions by selecting Thrown in the
Debug->Exceptions for Common Language Runtime Exceptions.
Once you've tracked down an Exception in code you have a few choices. You can
fix the error if it's something that you determine shouldn't be happening. If
it's due solely to the peculiarities of Blend and you want to leave the code as
is, except when running in Blend, WPF provides a way to check whether Blend (or
some other designer) is hosting the code. DesignerProperties.IsInDesignMode is
set to True by Blend and can be accessed from anywhere in a WPF application with
(bool)DependencyPropertyDescriptor.FromProperty(DesignerProperties.IsInDesignModeProperty, typeof(DependencyObject)).Metadata.DefaultValue
Any code that breaks Blend can just be executed conditionally when this value is
false.
One of the key advantages of using WPF is the promise of direct collaboration
between designer and developer. Fixing Blend rendering errors will enable design
and development of your application simultaneously without worrying about
keeping application code and static designs from separate teams in sync.
Saturday, February 02, 2008
#
I love developer tools. I've been an avid user of of Developer Express's CodeRush and Refactor Pro for years and have gotten so used to having them that their shortcuts have become reflexes and I get uncomfortable when I have to use Visual Studio without them. I've also come across a lot of devoted Resharper users who feel the same way about their favorite tool. The ongoing debate about which is better I think is a matter of personal preference. Both of these tools provide lots of extra capabilities to VS and I would recommend that anyone that spends a lot of time in Visual Studio try them both.
For a long time I wanted to try adding Resharper on top of the DevExpress tools but was always too worried about crippling CodeRush or eating all of my machine's resources. I had tried Resharper a few times in the past but had quickly given up when I saw how much memory overhead its background compiling added. A few months ago I started working with a team that all use Resharper and was impressed with some of the features it added, especially in the XAML editor. This finally gave me the extra nudge I needed to get the 2 products to work together and I'm happy to report that if you can afford to get them both and your hardware can handle it they do play well together.
I've now gotten quite comfortable with running the current versions of the DevExpress products (3.0) and Resharper (3.1) in both VS05 and now VS2008. A few tips if you're going to try them both:
- I've used the pattern of installing CR and Refactor, then Resharper. I ran into a problem at one point with losing some DevExpress settings after the Resharper install so I now zip up and then restore my entire Settings folder when installing Resharper.
- Both products have their own expansion template languages (like more powerful snippets). CodeRush includes many more templates out of the box and is my default choice but each template language has it's own strengths. My setup uses Space for CR expansion and Tab for Resharper expansion (I think those are both the defaults) so I can use either one or both for new templates, depending on which language fits the specific template I'm creating. CR's type substitution makes for more flexible templates in most cases. For templates that surround the current selection, Resharper's "Surround With" templates are quicker to add (but less flexible) than the corresponding CR feature.
- A few features common to both products can conflict: things like auto-closing braces and on the fly formatting. I got around a bunch of minor problems by turning off everything in the "Editor" options for Resharper. A lot of other common features (like refactorings and navigation) can be left on because they're triggered in different ways. The more you use them the more comfortable you'll get with mixing features from both.
- You will use more RAM. CodeRush and Resharper use somewhere in the neighborhood of 100-150 MB and Resharper uses about 75 MB plus more for background compiling depending on the size and complexity of the open solution. For large solutions Resharper can account for half of the total VS memory usage. A project I'm currently working on has a solution of ~20 projects that runs at about 850 MB with both tools running, 400 MB of which is used by Resharper. Both tools can be flipped on and off from the Add-in Manager if you want to temporarily do without their overhead and features.
- I have not tried doing a complete removal of both tools so if you're thinking about just trying this out do it in a VPC or on a system you wouldn't mind rebuilding. I've read about people having VS problems after removing past versions but I'm not sure how much uninstall has been improved recently for either product.
- Don't give up on either product if it feels like it's slowing you down at first! You will need some time to train yourself to do things in new ways but it will become much easier the longer you do it.
Here are some of my custom CodeRush templates for WPF in XAML and C#: CodeRush templates
These are some Resharper templates for XAML: Resharper templates
Get more info or download trials: Developer Express CodeRush and Refactor | Jetbrains Resharper
Sunday, January 27, 2008
#
Thanks to everyone that attended my presentations!
Sample code includes sln files for both VS 2005 and 2008. Slides are in pptx format (get converters to use in Office 2003).
WPF Data Binding: slides | code
Lots of stuff in the code that we didn't get to. Look at NavWindow.xaml to see just how much can go into a binding expression.
WPF Controls: slides | code
Also check out some of my other blogs posts for more sample code.
Tuesday, January 22, 2008
#
After trying out the Orcas Betas I was very disappointed that the XAML editor had actually been made harder to use, especially after using the VS 2005 November 2006
CTP addin for so long. There were some great additions to the editor and it seemed to be more
stable at least in the time that I spent in it but there were some usablity
differences
that I found infuriating and forced me to stop using the Betas for my
WPF development. Now that I've been using the RTM version for a while I'm happy
to report that the XAML editor is much more usable thanks to the new features that
made it into the final product.
Now if you've gotten used to the old XAML editor (or the plain XML editor, which
the CTP version was built on) closing your tags for you and adding quotes you won't need
to stop every 5 keystrokes to go back and add all the stuff you expected to be generated
when using Orcas. The new default view option is further icing on the cake. These issues were brought up by a lot of people during the Beta
process and obviously were added into the product near the end of the development cycle as indicated
by the separate Options dialog page for their settings as well as their absense
from the Betas. This really shows that MS does heed the feedback they get during
the CTP/Beta process.
A few additional WPF features that you get by moving from the 2005 CTP to 2008 RTM:
- The designer actually works more than 10% of the time! It's still quite possible
to "Whoops" it and it's still short of the rendering ability of Blend but it's a
big step from the CTP. It supports double-clicking to create event handlers but
unfortunately I haven't yet found a way to select anything other than the default
event like you can in Blend and in the WinForms/ASP.NET designers.
- Intellisense doesn't break when you reference a custom xmlns. It's also much more
geared toward XAML now that it's not based on the XML schema Intellisense. It also
works for adding xmlns definitions and lets you choose from a complete (more or
less) set of .NET namespaces in assemblies referenced by your project.
- Document Outline now works for XAML and even has an element level preview feature
that renders an element as you mouseover it in the outline.
The preview does require
that the design view be rendered but the outline itself is always there even if
you're staying in the text view. The outline breadcrumb familiar from the ASP.NET
designer also appears at the bottom of the editor in both modes and shares the same
preview functionality.
- Expanded syntax highlighting provides separate font settings for XAML which consist
of all the normal XML categories plus Markup Extension Class, Markup Extension Parameter
Name, and Markup Extension Parameter Value. There's also opening/closing tag highlighting
that works just like the bracket highlighting in C#.
- Formatting options let you set a tag wrapping length so elements with many attributes
will get wrapped to the next line or you can have every attribute placed on its own line.
- The Property dialog works for XAML. Like the Document Outline preview it requires
that the designer renders the XAML, but after that it can be used even in the text
only editor where it magically adds new attributes for any settings that you change.
This should be especially helpful for beginners who would otherwise have to waste
time digging through Intellisense to find the name of the property they want to
set.
- The integrated Zoom control lets you zoom in or out on the designer just like in
Blend. It also has a fit to window button that toggles to 100% and the maximum viewable.
- Solution Explorer knows that you're in a WPF project. Now if you right-click on
your project and Add -> User Control it gives you a WPF rather than WinForms
User Control!
- There is real backward compatibility with 2005 CTP so you don't need to be held
back by your poor coworkers that can't upgrade. When moving a project you will need
to run through the upgrade wizard the first time and you will need to plan on using
a separate 2008 solution as 8.0 and 9.0 slns are not compatible. The project upgrade
process will make a few changes including adding some new project properties (which
will be ignored by 2005), associating some new sub-elements (Generator and SubType)
with included XAML files, and removing the explicit reference to the Microsoft.WinFX.targets
MSBuild file. This last change is the only one that will mess with 2005 so once
the upgrade is completed, open the project as XML and add <Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets"
/> near the end of the file right below the CSharp/VisualBasic Import statement.
After that the project should work in both versions.
One additional tip: 2008 is much more stable on machines that have not had previous
Beta installations. I know it's a standard recommendation but my personal experience
has been that the WPF designer especially is
much more prone to crashing on my machines that were running Beta 2 and
had it uninstalled than my machines that were clean OS installs or only had 2005.
Monday, August 27, 2007
#
The flexibility of the WPF ItemsControls allows you build
column-row views of lists
of data by building templates to represent each row. Although a GridView used in a ListView
can also be used to provide these views the GridView introduces added complexity
along with its extra features, making it more difficult to style and sometimes causing
performance problems. To create a table layout with an ItemsControl (or ListBox) a top level
Grid should be set up with 2 rows to contain the column headers and the ItemsControl
itself. The Grids for the columns of the individual data rows are then set up inside
a DataTemplate that is applied to the ItemsControl.ItemTemplate property so it will
be repeated for each data item. This creates individual cells for each header and
data value but to get a usable table we need the columns for all these Grids
to line up properly.
There are a few different choices for lining up column sizes between these multiple
Grids. The simplest option is just to set fixed Pixel sizes for all of the columns and
make sure the ColumnDefinitions in the main Grid and DataTemplate Grid are the same.
The Width values can also be refactored as Doubles out to Resources so any changes
are applied to both Grids. Here's the ColumnDefinitions for both the headers and template:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="90"/>
<ColumnDefinition Width="65"/>
<ColumnDefinition Width="55"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
This is fine if you need something quick, but a fixed width,
non-resizable table isn't going to be something that's used very often. Making the
columns resizable would make a lot more sense. So what happens when GridSplitters
get added into this setup? The logical choice is to add them to the top level Grid
that defines the headers, but when these are used they only resize the headers themselves.
So how about inside the template? This is even worse because now you get individual
resizing for each column in each row! This method obviously won't work with resizing unless
we add a lot of code.
To get all of these different Grids to stay synchronized, WPF includes size sharing
functionality, accessed through the Grid.IsSharedSizeScope attached property and
the SharedSizeGroup property available on ColumnDefinition and RowDefinition. The
groups act like the GroupName on RadioButtons, with all elements that share a group
name working together to determine a common result. In this case, group names are matched anywhere under a parent
element that has IsSharedSizeScope set to true, which can even be our header Grid
in this example. In addition to getting rid of the pixel width definitions in our
template's Grid, this also causes all of the Grid columns to update when the header
Grid columns are resized using GridSplitters.
Here's the headers:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110" SharedSizeGroup="A"/>
<ColumnDefinition Width="90" SharedSizeGroup="B"/>
<ColumnDefinition Width="65" SharedSizeGroup="C"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="D"/>
<ColumnDefinition Width="*" SharedSizeGroup="E"/>
</Grid.ColumnDefinitions>
And in the template:
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition SharedSizeGroup="B"/>
<ColumnDefinition SharedSizeGroup="C"/>
<ColumnDefinition SharedSizeGroup="D"/>
<ColumnDefinition SharedSizeGroup="E"/>
</Grid.ColumnDefinitions>
We could just stop here and be happy with our resizable pixel-width columns, but
what if we want to use one of the other GridLength sizing methods: Auto or Star?
It turns out that Auto sizing works well with SharedSizeGroup because all it needs
to do when determining initial column width is take the largest width of any of
the "cells" in the group, which is probably the behavior you want most of the time.
The resizing GridSplitters don't add any more complication to this because once
they start modifying an Auto column width, it switches to a Pixel value anyway.
Now on to the most useful sizing method in WPF dynamic layouts: Star sizing. Unfortunately,
this is where the SharedSizeGroup method breaks down. If you want to have
a shared Grid that uses the whole available width and automatically adjusts when
that space changes you're going to need a different method. A column set to * in
a shared group acts just like an Auto column and won't fill or stay within the given
space. So what needs to happen to get back the Star sizing behavior?
Luckily ColumnDefinition.Width is a DependencyProperty, so it accepts Bindings.
By binding the template's columns to the header Grid's we can get back Star sizing
behavior and still keep them all in sync, even through resizes. The resizing
behavior can look a little strange if you have multiple * columns since they will
make whatever changes they need to to keep their size relative to each other the
same while still filling the available space, and do this in real time as you drag.
This isn't a problem in itself, but may confuse your users, so consider only using
a single * column in a resizable setup like this. The FindAncestor
binding used is a long one, but stays the same for each definition with the exception of the collection index
on each column. The header's definitions:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="65"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
And the template's:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[0].Width}" />
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[1].Width}"/>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[2].Width}"/>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[3].Width}"/>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[4].Width}" />
</Grid.ColumnDefinitions>
The Binding method is more of a dictatorship than the SharedSizeGroup's
democratic method of deciding sizing. Rather than taking the widest column value from
the whole group, Binding just passes the header Grid's definition (the source) down to
the template's Grids (the targets), in this case our smart * values.
Unfortunately we still have a problem with the current solution. Although the Binding passes its value exactly,
that value can be misinterpreted by the bound columns. If you use the Binding method
on a column using Auto sizing, the bound value will be Auto, rather than the calculated
width of the header column. Each cell will then set its own size to Auto and determine
its actual width based on its own content. This new problem resolves itself quickly
if the Auto column is resized, because at that point the header reverts to a fixed
Pixel size which will trickle down to all of the child columns. The thing that saves
this Binding method is the fact that the problem only comes up with Auto sized columns,
which are handled correctly by SharedSizeGroup, so using the two methods in combination
allows the use of any GridLength sizing method and manual resizing. Here's the final
combined header:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="B"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="C"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="D"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
And the template:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[0].Width}" />
<ColumnDefinition SharedSizeGroup="B"/>
<ColumnDefinition SharedSizeGroup="C"/>
<ColumnDefinition SharedSizeGroup="D"/>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[4].Width}" />
</Grid.ColumnDefinitions>
Download sample code demonstrating all these methods here.
Thursday, June 21, 2007
#
In
Part 1 I covered the window settings and the main activity display. We're now
ready to look at the ActivityDataObject detail displays. Details are stored as an XML string
in ActivityDataObject and consist of a Name and Description. A ComboBox lists all
activities by name and determines which activities are displayed in the other controls.
A TabControl shows 2 views of the details: a ListBox and a customized HeaderedItemsControl.
There is also a pair of TextBoxes and a Button to add new details to the selected
activity.
<StackPanel Grid.Column="1" Grid.Row="1" Margin="15,5,5,5">
<TextBlock FontSize="12" Text="Activity Detail Settings" Foreground="Firebrick" HorizontalAlignment="Center" Margin="0,5,0,5"/>
<ComboBox x:Name="detailItemList" ItemsSource="{Binding Activities}" DisplayMemberPath="Name" IsSynchronizedWithCurrentItem="True"/>
<TabControl Height="170" Margin="0,5,0,0">
<TabItem Header="ListBox">
<ContentControl ContentTemplate="{StaticResource DetailContentTemplate}" Content="{Binding Activities}" />
</TabItem>
<TabItem Header="ItemsControl" DataContext="{Binding ElementName=detailItemList, Path=SelectedItem}">
<TabItem.Resources>
<local:DetailDisplaySelector x:Key="DetailDisplaySelector" DefaultTemplate="{StaticResource DetailBodyDefaultTemplate}" AlternateTemplate="{StaticResource DetailBodyAlternateTemplate}"/>
<local:DetailsHeaderSelector x:Key="DetailsHeaderSelector" DefaultTemplate="{StaticResource DetailHeaderDefaultTemplate}" AlternateTemplate="{StaticResource DetailHeaderAlternateTemplate}"/>
</TabItem.Resources>
<HeaderedItemsControl DataContext="{Binding Path=Details, Converter={StaticResource XmlStringToXmlDocumentConverter}}"
ItemsSource="{Binding Path=/}" Header="{Binding ElementName=detailItemList, Path=SelectedItem}"
HeaderTemplateSelector="{StaticResource DetailsHeaderSelector}" ItemTemplateSelector="{StaticResource DetailDisplaySelector}"
Style="{StaticResource HeaderedItemsControlStyle}"/>
</TabItem>
</TabControl>
<DockPanel Margin="0,10,0,0">
<Label Content="Name" DockPanel.Dock="Left"/>
<TextBox x:Name="detailName" Text="name"/>
</DockPanel>
<DockPanel Margin="0,5,0,5">
<Label Content="Description" DockPanel.Dock="Left"/>
<TextBox x:Name="detailDescription" Text="description"/>
</DockPanel>
<Button Content="Add Detail" Command="{Binding ElementName=detailItemList, Path=SelectedItem.AddDetailCommand.Command}"
local:CommandBinder.Command="{Binding ElementName=detailItemList, Path=SelectedItem.AddDetailCommand}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource ConcatenateStringsMultiConverter}" ConverterParameter=";">
<Binding ElementName="detailName" Path="Text"/>
<Binding ElementName="detailDescription" Path="Text"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
</StackPanel>
There are a lot of new concepts being used here. First we'll look at the ListBox
TabItem. Instead of using a ListBox directly here there's a ContentControl
using a ContentTemplate. By binding this to the same Activities collection used
by the ComboBox we get just the single item that is selected by the ComboBox passed
to the ContentTemplate. Here's the template:
<DataTemplate x:Key="DetailContentTemplate">
<StackPanel DataContext="{Binding Path=Details, Converter={StaticResource XmlStringToXmlDocumentConverter}}">
<ListBox ItemsSource="{Binding XPath=DetailList/Detail}" ItemTemplate="{StaticResource XmlDetailTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" MaxHeight="135"/>
</StackPanel>
</DataTemplate>
Here we see the first usage of a nested DataContext. To get binding access to the
XML detail data a converter is used to create an XmlDocument from the Details string
at the StackPanel level.
The ListBox can then use XPath Binding to get to the individual details. Now each
list data item consists of a single Detail element with Name and Description child
elements available for XPath Binding.
The other TabItem uses another nested DataContext, this time bound directly to the
ComboBox's SelectedItem. This DataContext could also bind directly to Activities
with the same result, thanks to automatic current item synchronization. A HeaderedItemsControl
uses two TemplateSelectors which are declared in the TabItem's Resources. These
selectors choose from a Default or Alternate template which are passed in as properties.
Here's the code from the header selector:
public class DetailsHeaderSelector : DataTemplateSelector
{
private DataTemplate _DefaultTemplate;
public DataTemplate DefaultTemplate
{
get { return _DefaultTemplate; }
set { _DefaultTemplate = value; }
}
private DataTemplate _alternateTemplate;
public DataTemplate AlternateTemplate
{
get { return _alternateTemplate; }
set { _alternateTemplate = value; }
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
ActivityDataObject dataItem = item as ActivityDataObject;
if (dataItem == null)
return null;
if (dataItem.Name == dataItem.Name.ToLower())
return AlternateTemplate;
return DefaultTemplate;
}
}
This method removes the usage of hardcoded template names and avoids exceptions
that may be thrown by using FindResource. The HeaderedItemsControl also uses a further
nested DataContext of its own that sets up the XmlDocument. The Header binds to
the selected ActivityDataObject outside of the DataContext and the ItemsSource binds
to the XmlDocument to access the list of details. Finally, a Style is applied to
set up the template for the control. The default template for a HeaderedItemsControl
doesn't actually display the content assigned to the Header so this needs to be
replaced here with a custom template. Here's the template part of the Style:
<ControlTemplate TargetType="{x:Type HeaderedItemsControl}">
<DockPanel>
<ContentPresenter DockPanel.Dock="Top" Content="{TemplateBinding Header}" ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}" ContentTemplate="{TemplateBinding HeaderTemplate}"/>
<Border DockPanel.Dock="Bottom" SnapsToDevicePixels="true" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</DockPanel>
</ControlTemplate>
A ContentPresenter is added to hold the Header content and an ItemsPresenter is
used to implicitly bind to the ItemsSource. The items and header both change their
appearance based on whether their data items have an all lowercase name. In flipping
through the detail data you'll notice the different text styles being applied by
the selected templates. The last control in this section of the app is a button
for adding details to the selected activity. Here's its XAML again:
<Button Content="Add Detail" Command="{Binding ElementName=detailItemList, Path=SelectedItem.AddDetailCommand.Command}"
local:CommandBinder.Command="{Binding ElementName=detailItemList, Path=SelectedItem.AddDetailCommand}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource ConcatenateStringsMultiConverter}" ConverterParameter=";">
<Binding ElementName="detailName" Path="Text"/>
<Binding ElementName="detailDescription" Path="Text"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
Since all of our data and logic is being held in the ViewModel class rather that
the code-behind we don't want to use a Click event handler for the button (event
handler methods can only exist in the code-behind of the XAML file using them, in
this case Window1.xaml.cs). We also
want to perform an action on a specific data item that is selected in another control. To accomplish this a Command is used to
bind the Button's click to an action in the ViewModel's data. This is an important part of the separation in
the M-V-VM pattern. The command technique used here is based on an example on
Dan
Crevier's blog. Look at the CommandModel, CommandBinder, and ActivityDataObject
classes to
see how this works. The AddDetailCommand property exists on the ActivityDataObject
itself and creates a new detail when its Command is executed. This way the WindowViewModel
class doesn't even need to keep track of the selected item outside of the ICollectionView
itself. Although not the usual method of using commands this works well for maintaining
the separation of the ViewModel and View. The MultiBinding here
puts together the name and description fields to pass into the single CommandParameter
property (there are many different ways to use this property since it is an Object).
The last two sections provide controls for setting the global State override and
for filtering the displayed activities.
<Border Grid.Row="2" Margin="5">
<StackPanel>
<CheckBox x:Name="GlobalStatesCB" Content="Set all activity states" IsChecked="{Binding UseGlobalState}" />
<StackPanel Orientation="Horizontal" IsEnabled="{Binding ElementName=GlobalStatesCB, Path=IsChecked}">
<RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Off}" Content="Off" Margin="5"/>
<RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Ready}" Content="Ready" Margin="5"/>
<RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Starting}" Content="Starting" Margin="5"/>
<RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=On}" Content="On" Margin="5"/>
<RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Stopping}" Content="Stopping" Margin="5"/>
</StackPanel>
</StackPanel>
</Border>
<Border Grid.Row="2" Grid.Column="1" Margin="5">
<StackPanel>
<TextBlock Text="Filter Out Categories"/>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=FilterCategory1, Mode=OneWayToSource}" Content="1" Margin="5"/>
<CheckBox IsChecked="{Binding Path=FilterCategory2, Mode=OneWayToSource}" Content="2" Margin="5"/>
<CheckBox IsChecked="{Binding Path=FilterCategory3, Mode=OneWayToSource}" Content="3" Margin="5"/>
<CheckBox IsChecked="{Binding Path=FilterCategory4, Mode=OneWayToSource}" Content="4" Margin="5"/>
</StackPanel>
</StackPanel>
</Border>
The GlobalState RadioButtons use the same method shown in the Category2 template
but bind to a different property. Changes to this property trigger code in the ViewModel
that sets the State value for all activities. The FilterCategory CheckBoxes each
bind to separate boolean properties, all of which are used in the Activities CollectionView
Filter. Here's the code for one of the properties along with the filtering method
and the CollectionView initialization:
public bool FilterCategory1
{
get { return _filterCategory1; }
set
{
if (_filterCategory1 == value)
return;
_filterCategory1 = value;
OnPropertyChanged("FilterCategory1");
Activities.Refresh();
}
}
private bool FilterMethod(object checkItem)
{
ActivityDataObject activity = checkItem as ActivityDataObject;
if (activity == null)
return false;
if (activity.Category == ActivityCategory.Category1)
return !FilterCategory1;
if (activity.Category == ActivityCategory.Category2)
return !FilterCategory2;
if (activity.Category == ActivityCategory.Category3)
return !FilterCategory3;
if (activity.Category == ActivityCategory.Category4)
return !FilterCategory4;
return false;
}
public WindowViewModel()
{
_activities = new ObservableCollection<ActivityDataObject>(DataLoader.GetXmlData());
_activitiesView = CollectionViewSource.GetDefaultView(_activities);
_activitiesView.Filter = new Predicate<object>(FilterMethod);
_useGlobalState = false;
_globalState = ActivityState.Off;
}
Each of the FilterCategory setters refreshes the Activities CollectionView to update
the filtering. WindowViewModel also implements the INotifyPropertyChanged interface to
send notices to WPF when bindings need to be updated. This is what the OnPropertyChanged
call is for. Activities wraps _activitiesView which is actually an ICollectionView
that is created by CollectionViewSource's GetDefaultView static method.
Try out the app and try making some changes of your own. The activity data is loaded
on startup from a TestData.xml file so you can easily try out different data settings.
Window1.xaml can load in Blend RTM so you can try out all of the functionality Blend
makes available for UI and data binding editing. When looking at the WindowViewModel
code notice how there are no references to any part
of the XAML UI. This type of class is easy to unit test and can be hooked up to
completely different XAML or even a WinForms or web UI! Here again
is the code.