Saturday, March 21, 2009

WPF: Loading Generalized DataTemplates in Code

I've been working WPF quite a bit recently and am discovering the ins and outs of this beast. It's nothing if not ambitious.

I personally like it. A lot. I worked with .NET Forms previously and Win32, and Java Swing before that, and this is far and away the most sophisticated UI framework I have ever seen. However, it's still relatively new and documentation (as well as good examples) can be scarce.
One thing that I've noticed when browsing examples online is that most people present ideas through trivial examples - in fact, they repeat the same information through repeated trivial exercises that can be found on other sites. I'll try to avoid doing that, but that also means that I'm going to assume that you know what you're doing. I'm not going to post tutorials or explain basic WPF concepts that are covered in the all the main WPF resources (see another post in the future.) I'm going to post annoying factoids that have halted progress for me personally at work, and hopefully this will help someone out there.
Let's jump in and take a look at generalized DataTemplates in WPF. By generalized, I'm talking about the DataTemplates that set their type via the DataType property. Sampled from MSDN:
<DataTemplate
  DataType="{x:Type c:GreekGod}">
   <TextBlock Text="{Binding Path=Name}" Foreground="Gold"/>
</DataTemplate>
Like the Style element, DataTemplate uses the DataType property as a way of hooking itself into the general scope of your application. Style's version of this is TargetType.
Now, here's the problem: Where do you specify this DataTemplate? Where does it go in the project? No one seems to make this clear, or provides a satisfactory answer. The real answer is that this DataTemplate needs to exist in a loaded Resources dictionary somewhere up the logical chain of elements where you expect it to be used.
Everyone assumes a trivial layout for a project. "If you need to create a DataTemplate, then just add it to the Window.Resources element, dumbass." However, this is not very helpful, for anyone who is working outside the standard layout conventions (for instance if you're developing WPF controls for a mixed managed application, and blending them into a Win32 frame.) Sometimes, you don't have the benefit of simply adding the template to a higher level component because, wait for it, the XAML might actually be dynamic. Maybe your application reads in XAML to construct a class at runtime. Maybe popping in a DataTemplate definition here is not such a good idea.
So now I'm scratching my head and going back to the root of the problem - how does WPF know about this template in the first place? If I put it in a loose XAML file in my project, and have it compiled, shouldn't the template be properly registered at runtime? Turns out that that isn't the case. Loose XAML does not a loaded component make, which I thought was interesting, and actually makes a lot of sense. This is something that may not be clear from MSDN: Just because you put a DataType property on a DataTemplate in a ResourceDictionary element without a x:Key in a XAML file that is compiled into your project, doesn't mean that it's actually registered with WPF at runtime. It needs to be loaded into the logical chain somewhere.
XAML is cool, but it's still a static definition of components. If you're going to need to sew anything in, you're best bet is to go back to code.
In my case, the answer lay in dynamically loading the XAML and adding it to my root control's MergedDictionaries set (no handy top-level Window, sorry), in the root control's constructor.
To do this, you create a Uri object that points to your project XAML resource (compiled BAML, actually), and use the Application.LoadComponent() static method to load and create the root node of the XAML. If you store your DataTemplate in a ResourceDictionary, the code might look something like this:
public RootControl() {
 InitializeComponent();
 Uri uri = new Uri("MyAssembly;component/Templates.xaml", System.UriKind.Relative);
 Resources.MergedDictionaries.Add((ResourceDictionary) Application.LoadComponent(uri);
 
 // ... continue initialization
}
Take a look at the MSDN Uri class definition for details as to why this syntax works. The above code is written for an assembly called "MyAssembly.dll", and with a XAML file called Templates.xaml in the root of the project. If you place your loose XAML files in subfolders, add the path after "component" in the Uri. "component" is really just the root of your project.
MergedDictionaries is a handy way of importing and aggregating resources from different definitions into a single Resources object. By loading the XAML into the root component's merged dictionary, you are indirectly registering the DataTemplate into the current logical scope, meaning it will be applied to your instantiated data.
A note concerning Merged Dictionaries and performance. If you have many resources and they are merged into a control that instantiated many times over, be aware that each Merged Dictionary is created once per component - you may want to look into ways of statically organizing your resources. I'll probably be looking at this on my side and will share any advice concerning this in future posts.
Of course, if your root class has a XAML definition, then go ahead and use this simpler notation:
<RootControl x:Class="UserControl"
 xmlns="..."
 xmlns:x="..." >
<RootControl.Resources>
 <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
       <ResourceDictionary Source="Templates.xaml"/>
    </ResourceDictionary.MergedDictionaries>
 </ResourceDictionary>
 </RootControl.Resources>
</RootControl>
And that's that. Hopefully this post manages not only to convey how to load templates via code, but also provides a bit of insight into how WPF does things under the hood.

2 comments:

johnds,  June 16, 2009 at 6:36 AM  

Hi Dave,

Nice post, good set of information. I totally agree with you on your comments about WPF/XAML documentation and those people giving generalzed examples, no matter the number of incarnations of those examples.

One thing that is really starting to grind my diodes is the lack of code infrastructure for resource dictionaries, datatemplates and such.

For instance, if you define a DataTemplate WITHIN a ResourceDictionary xaml file (ie the top level element is a ResourceDictionary), and you use DataTemplates defined within that RD (like the Generic.xaml file for example), from say, the xaml file of another control, there seems to be no way to access the objects defined in the said templates.

Templates, are quite limiting in the sense that there is no code-behind generated for them, so if you *need to* get control of/access to a UI element within the template, you cant! Or at least, I have not been able to yet.

Unless you have discovered, deep inside the cave of WPF some magical way of getting hold of objects defined in the template?

Dave Cunningham June 17, 2009 at 6:47 PM  

Hey John,

Thanks for the comments. That, of course, a tough question. The short answer is that you can traverse up and down the visual / logical tree using these classes:

System.Windows.LogicalTreeHelper
System.Windows.Media.VisualTreeHelper

But the long(er) answer is that you should avoid doing this as much as possible. I was working on a problem that I thought would require me to drill down to a particular visual just today, but eventually found a much more palatable solution. In my case, I was working with a TreeView with a solid ViewModel back-end implementation, based on this post:

http://www.codeproject.com/KB/WPF/TreeViewWithViewModel.aspx

(Great post, recommended reading.)

My problem was that I was setting the selection on the treeview programmatically and needed to be able to center the scrollable area on my new selection. However, I wasn't literally setting the selection on the treeview, but rather on my underlying treeviewmodel that was bound to visual representation of this tree. However, the function I needed to call (FrameworkElement.BringIntoView()) wasn't a property that could be bound, and it didn't make for a good datatrigger candidate via the ObjectDataProvider class.

It seemed rather simple, right? I needed to get the currently selected visual, and call BringToView() on it. But how to do that if I haven't, as of yet, even touched one of the visuals directly?

I thought I would end up drilling down to find the visual, but I found this very useful post instead:

http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/36aca7f7-0b47-488b-8e16-840b86addfa3

The solution was to simply attach a Selected event handler (which is an event on TreeListItem) onto the TreeList itself, and let it catch the bubbled event. Once caught, I could refocus the treelist as appropriate.

I guess the bottom-line is if you find yourself drilling down to find visuals, it's usually a sign that you should re-examine how you've split your model and views. Of course, easier said than done, most times.

Dave

Post a Comment

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Back to TOP