John Bowen's Blog

Blog.WriteLine(new Random().NextThoughts(".NET"));

My Links

Blog Stats

News

Archives

Post Categories

Image Galleries

Dev Tools

MSBuild

Team Foundation Server

WPF

Wednesday, October 01, 2008 #

New Blog Site

My blog has moved to team.interknowlogy.com/blogs/johnbowen/

Check it out for recent posts!

posted @ 4:03 PM

Sunday, June 29, 2008 #

San Diego Code Camp 2008 Resources

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

 

posted @ 2:45 PM

Friday, May 30, 2008 #

Getting to Know Your Resource Tree

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.
Resource Diagram
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).

posted @ 11:48 AM

Thursday, May 22, 2008 #

Silverlight 2 and DHTML

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.

Toolbar

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. **

posted @ 10:15 PM

Sunday, May 11, 2008 #

Debugging Blend Errors

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.

Blend XAML Error

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.

Exception Options

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.

posted @ 5:13 PM

Saturday, February 02, 2008 #

Using CodeRush and Resharper together

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

posted @ 2:30 PM

Sunday, January 27, 2008 #

Code Camp Presentations

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.

posted @ 5:43 PM

Tuesday, January 22, 2008 #

Moving to Visual Studio 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.
    Document 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.
    Breadcrumb
  • 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.
    Property Grid
  • 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.

posted @ 12:17 AM

Monday, August 27, 2007 #

Grid Size Sharing in WPF

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.

posted @ 8:22 PM

Thursday, June 21, 2007 #

More WPF Data Binding, Part 2

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.

posted @ 11:47 PM