Monday, January 11, 2010

Dynamic ControlTemplate in Silverlight

I have a Silverlight 3 application that uses a grid from DevExpress. I needed to extend the standard AgDataGridColumn so that i could show images, those images being representations of an enumeration.

To achieve this is simple enough - just override the ControlTemplate that is assigned to the DisplayTemplate property of the column. Doing this declaratively (in XAML) is pretty damn simple. But i wasn't doing it that way - as i am using a factory pattern for creating the columns from some agnostic descriptors, i needed to locate, load and assign the ControlTemplate programmatically.

The extended grid column sits in a second assembly. As i had created several templated controls in this controls assembly, the project templates had already created a ResourceDictionary called generic.xaml in the Themes folder, so i intended to use this file to define the ControlTemplate and then just load it in code, using a line of code similar to


ControlTemplate ct = Application.Current.Resources["MyImageColumnTemplate"] as ControlTemplate;

Unfortunately, this didn't work, Application.Current.Resources had no idea about my ControlTemplate. Then i realised that because it was defined in a ResourceDictionary, i needed to load that ResourceDictionary and merge it with the resources associated with the current application. To do that is pretty simple:

ResourceDictionary dict = new ResourceDictionary();
dict.Source = new Uri("path to resource dictionary", UriKind.Absolute);

Application.Current.Resources.MergedDictionaries.Add(dict);


But of course nothing is that simple, right? I found half a dozen blog or forum postings that indicated all i would have to use for my URI was this:

Uri uri = new Uri("pack://application:,,,/MySilverlightControls;component/themes/generic.xaml", UriKind.Absolute);

The scheme is correct, the assembly name is correct, the path is correct, what could go wrong? Well, first i received an error about there being no port number specified:


UriFormatException was unhandled by user code

Invalid URI: A port was expected because of there is a colon (':') present but the port could not be parsed.

A little bit more googlebinging showed me that i probably needed to register the pack scheme – i thought it would already be registered (especially as the Loaded event for several Silverlight components had been fired) but it wasn't. OK, another half step forward:

if (!UriParser.IsKnownScheme("pack"))

UriParser.Register(newGenericUriParser(GenericUriParserOptions.GenericAuthority), "pack", -1);


Once i dropped that bit of code in an appropriate place, that error went away. But then i started to be plagued with vague COM errors depending on how i tried to declare the URIs.


dict.Source = new Uri("/themes/generic.xaml", UriKind.Relative);


dict.Source = new Uri("./../../themes/generic.xaml", UriKind.Relative);


dict.Source = new Uri("../../themes/generic.xaml", UriKind.Relative);

all produced the error


Error HRESULT E_FAIL has been returned from a call to a COM component.


at MS.Internal.XcpImports.CheckHResult(UInt32 hr)


at MS.Internal.XcpImports.SetValue(INativeCoreTypeWrapper obj, DependencyProperty property, String s)


at etc, etc...



dict.Source = new Uri("pack://application:,,,/themes/generic.xaml", UriKind.Absolute);


dict.Source = new Uri("pack://application:,,,/MySilverlightControls;component/themes/generic.xaml", UriKind.Absolute);

both produced the error


Exception from HRESULT: 0x80072EE5

which effectively means "Invalid URI".

So i did what any engineer or scientist should do – remove every variable from the equation until you are left with just the single problematic item, and then try and isolate the problem. I created a new ResourceDictionary
(called Dictionary1.xaml) under the Themes folder, put the MyImageColumnTemplate XAML in there, and went back through my seven different ways of specifying the URI for the ResourceDictionary. Suddenly i had success! This ended up being the code that worked:

XAML:


<ControlTemplate x:Name="MyImageColumnTemplate" >

<Grid MaxHeight="20" MaxWidth="20">

<Grid.Resources>

<localGrid:EnumColumnImageConverter x:Key="ImageContentConverter"/>

</Grid.Resources>

<Image Source="{Binding EditValue, Converter={StaticResource ImageContentConverter}}" />

</Grid>

</ControlTemplate>


C#:

Uri uri = new Uri("/MySilverlightControls;component/themes/Dictionary1.xaml", UriKind.Relative);

ResourceDictionary dict = new ResourceDictionary();

dict.Source = uri;

Application.Current.Resources.MergedDictionaries.Add(dict);

ControlTemplate ct = (ControlTemplate)Application.Current.Resources["MyImageColumnTemplate"];

this.DisplayTemplate = ct;

That URI also worked when i pointed it at generic.xaml. All the other attempted URIs look correct, but none of them would work. Now i had a custom image column that used my custom ControlTemplate, which in turn used my Converter to translate an enumeration into the appropriate image (that image was also extracted from a resource file.... but i had no issue with that!).


Wow, this post was a bit long but i tried my best to keep it simple, and i hope it helps someone out there.






7 comments:

Anonymous said...

Thank you. Thank you very much. I spent forever trying to do this. I looked at 100 different approaches with no luck. I'm surprised its this convoluted in the first place.

Anonymous said...

Solutions:
1. Static markup in XAML there
2. For dynamic loading there

Anonymous said...

The post is fabulous though i am having some problems in implemention it. Can i hv the source code in C# of the above so that i can use it in my code.

dhaval_samaranayeke@glic.com

Anonymous said...

Hi there,

What is this "MySilverlightControls" in your code? Is it your project name or any namespace?? I am also not able to get the uri of the dictionary resource that is y i asked.

shane said...

I can't give you the source, as this is a cut down version of something much larger. I would suggest you jump on stackoverflow.com and ask your question, on there you will be able to post the exact bit of code you are having issues with so that myself or others can check it over for you.

shane said...

"MySilverlightControls" is the default namespace of the referenced assembly containing the template i want to use - the assembly in this case is called "MySilverlightControls.dll". If you need to, take some of the key pieces of this post and cross reference them with the examples in MSDN, it may help you spot where you need to insert your own values, and what to use for those values.

Greg said...

Has anyone figured out how to programmatically create a resource dictionary? A dictionary where no .xaml file exists during design time.

please let me know

thanks much
Greg