How to Create Custom Theme

Posted 11 years ago by Avatar David Mullin
We are attempting to consume the Actipro ThemeManager as the central theme controller for our application (rather than use the ThemeManager for the Actipro controls, and our own manager for everything else). The documentation suggests that we should be able to do this by creating our own ThemedResourceDictionary-derived class. However, there are no examples of doing this, and the documentation is a tad sparse. And, since you have wisely obfuscated your code, we can't use Reflector to see what you did.

Or, is what I am trying to accomplish not really the intended use of the ThemesManager? I mean, it seems to me that I should be able to define the appearance for all of the standard and custom controls that I want and register that as the "AeroNormalColor" theme, and have a different appearance defined which I have registered as the "Office2007Black" theme - so that when I tell the Theme Manager to use that theme, not only do the Actipro controls change, but all of the other controls in my application also change.

If there is a sample online somewhere that you can point me, that would be really helpful.

My initial questions are:

1) What is the LocationUri property expected to return? I have attempted to have it say:
return new Uri(@"Themes\Earth\EarthResources.xaml");
(since the Resource Dictionary that contains my theme definition is in that path in the project). However, when I do this, I get a System.UriFormatException: "Invalid URI: The format of the URI could not be determined." I also attempted:
return new Uri(@"pack://siteoforigin:,,,/Themes\Earth\EarthResources.xaml");
(since I wanted to allow the actual theme contents to be read in at run time), but I got a System.ArgumentException Cannot use absolute URI.".

2) If I want to have my theme apply a style/template to a standard control - say, Button - do I have to do anything special to make this happen? Normally, I would have added the EarthResources Resource Dictionary to App.xaml, and since that contains a style for Button, it would just get applied. One presumes that there is something that must be done so that a control knows it is part of a given "Group" (or, perhaps I just don't really understand the purpose/function of the Group property on the Theme...)

I realize that these are rather broad questions, but hopefully you can at least point me in the correct direction.

Thank you in advance.

David Mullin

Comments (14)

Posted 11 years ago by Actipro Software Support - Cleveland, OH, USA
Hi David,

Yes, ThemeManager is designed to support any controls including your own. We had wanted to get a demo for it in the Sample Browser but didn't have it finished in time for v3.5 However there is a demo that is ready for the next maintenance release that shows how to make multiple themes for a custom control and allow the operating system to dictate which one is used (normal WPF behavior) as well as force a specific theme to be used. If you'd like, email over and we can ZIP up the code for you.

The demo is intended to complement the documentation on the ThemeManager. It may be helpful for you to look the demo over with the documentation and reply back whether everything is more clear or if additional documentation is needed.

For your questions...

1) Here is the best way to return the Uri:
return new Uri(String.Format(CultureInfo.InvariantCulture, "/{0};component/Themes/ThemedCustomControl/Classic.xaml", Assembly.GetExecutingAssembly()), UriKind.Relative);
So in that case, the resource dictionary file Classic.xaml is located in this project folder:

2) The ThemeManager at its base is simply a resource dictionary manager. You are registering certain resource dictionaries with theme name / group combinations. So when you change the ThemeManager.CurrentTheme property, it finds all the registered groups for that theme, unloads any existing themes, and loads up the located themes.

Groups are used to categorize groups together. For instance all our Ribbon controls are in a "Ribbon" group. NavigationBar is in a "NavigationBar" group and ExplorerBar is in an "ExplorerBar" group.

Hope that helps!

Actipro Software Support
Posted 11 years ago by David Mullin
Thanks for the information, I do believe that will help me a great deal. I have send an email to Actipro Support asking for that zip file - I look forward to getting it.

Posted 11 years ago by David Mullin
Is there any way to get it to accept an absolute URI?

If I say this:
return new Uri("/ActiproThemes;component/Themes/Earth/EarthResources.xaml", UriKind.Relative);
it works perfectly. However, this requires that EarthResources.xaml be actually compiled into my assembly. We would like to be able to have the contents of our theme reside outside a compiled assembly (for easier customization).

Prior to using ThemeManager, we did this by loading the resource dictionary with this Uri:
return new Uri(@"pack://siteoforigin:,,,/Themes/Earth/EarthResources.xaml", UriKind.Absolute);
However, ThemeManager complains that this must not be an absolute Uri.

Why is that? Is this by design? Is there any good way to work around it? It seems to me that what might work is to have the dictionary that is compiled in just include the external file (i.e., add another level of indirection), but it would be nice if there were a more direct means of doing this...

Posted 11 years ago by Actipro Software Support - Cleveland, OH, USA
I don't think it's our code that throws that exception. What is the stack trace?

Here is how we load the resources:
ResourceDictionary resources = Application.LoadComponent(this.LocationUri) as ResourceDictionary;

Actipro Software Support
Posted 11 years ago by David Mullin
True enough - Application.LoadComponent() will throw an exception if given an absolute Uri. Odd, though, the documentation for that method says it can take a site-of-origin file...

Clearly, something I need to investigate more, but you are correct, this is not an Actipro issue.

Thanks for the prompt reply!

Posted 11 years ago by Actipro Software Support - Cleveland, OH, USA

One note, we've found that our Uri code that we posted in one of the original replies above for creating the LocationUri is slightly incorrect. In the upcoming maintenance release we've added a static ThemedResourceDictionary.GetLocationUri method in which you pass an assembly instance and the relative path of the XAML file and it will properly construct the Uri.

Calling conventions are like this and we urge you to use our helper method once the next maintenance release is out:
return ThemedResourceDictionary.GetLocationUri(Assembly.GetExecutingAssembly(), "Themes/ThemedCustomControl/Classic.xaml");

Actipro Software Support
Posted 11 years ago by David Mullin
So close, and yet so far away...

I'm having an issue that is possibly related to the reason you implemented this change. All of my themes are in a DLL which is consumed by my application. In the DLL, I have a Registrar class, and it is attempting to register my themes. However, I am getting an exception that it could not find the resource. My URI code looks like this:

return new Uri(String.Format(CultureInfo.InvariantCulture, "/{0};component/Themes/Smurf/SmurfResources.xaml", Assembly.GetExecutingAssembly()), UriKind.Relative);

And that is, in fact, the location of the XAML file inside my project.

Is this related? Is there something that I am doing wrong? Is there any workaround? When is the next maintenance release due out?

Posted 11 years ago by Actipro Software Support - Cleveland, OH, USA
It could be. One problem with the line you posted is that GetExecutingAssembly is doing a ToString which puts the FullName in the Uri. Per pack syntax, it should just be the assembly name (no version, culture info, etc). However you optionally can append version (which we do in our method) but it uses a different format than the Assembly.ToString(). As a workaround for now, instead of Assembly.GetExecutingAssembly(), just put in the name of the assembly.

We hope to have the maintenance release out next week.

Actipro Software Support
Posted 11 years ago by David Mullin
Thank goodness you're still on the clock! I've been doing the integration to the ThemeManager all week, and I'm completely dead in the water (which means the rest of the team is kinda dead, too!)

Actually, my first try was to use the name of the Assembly, when that blew up, I figured I'd try the GetExecutingAssembly() thing, since that's what you gave me originally.

The exact exception I am getting is a System.IO.IOException: Cannot locate resource 'themes/earth/earthresources.xaml'.

My revised URI code is:
return new Uri("/CaseTrakker.Windows;component/Themes/Earth/EarthResources.xaml", UriKind.Relative);
I realize that it is late on a Friday, but I'd like to get things put back together for the team by Monday morning...
Posted 11 years ago by David Mullin
Well, I'm pretty sure that this isn't actually an Actipro problem.

This code works fine:
Uri uri = new Uri("/CaseTrakker.Windows;component/LoginWindow.xaml", UriKind.Relative);
This code throws an exception:
Uri uri = new Uri("/CaseTrakker.Windows;component/Themes/GlobalImages.xaml", UriKind.Relative);
If you see something that I'm missing, that'd be great. If not, well, I'm having a problem with stock WPF stuff, so it's clearly my problem...
Posted 11 years ago by Actipro Software Support - Cleveland, OH, USA
With our new helper method code, the sample Uri I originally posted will be this now:
new Uri("pack://application:,,,/SampleBrowser;v3.5.422.0;component/Themes/ThemedCustomControl/Classic.xaml", UriKind.RelativeOrAbsolute)
There is a topic in the MSDN on pack:// syntax, which is what all these Uri's use. Check that out as it may help you more.

Actipro Software Support
Posted 11 years ago by David Mullin
Thanks. My first attempt at using that pack syntax didn't work, but I'll go and read the MSDN stuff.

There is clearly something wonky going on in WPF-land.

If I say this, it works:
uri = new Uri("/CaseTrakker.Windows;component/Themes/CommonStyles.xaml", UriKind.Relative);
If I say this, it throws an exception:
uri = new Uri("/CaseTrakker.Windows;component/Themes/GlobalImages.xaml", UriKind.Relative);
However, if I move the GlobalImages.xaml file into the Styles folder, this works:
uri = new Uri("/CaseTrakker.Windows;component/Styles/GlobalImages.xaml", UriKind.Relative);
Bizarre. Almost seems like a bug in WPF...


PS - Thanks for all your help.
Posted 11 years ago by David Mullin
Note to self: when taking over a body of work from another programmer - particularly a junior one that was running off in a whacky direction - make sure you fully understand all of the little things they were doing.

And, why wasn't it work? Because some of the xaml files had a build action of "None" rather than "Page" - because the scheme was to deploy the xaml files with the application and get discovered at runtime. We abandoned this scheme in favor of ThemeManager, but the files hadn't been changed back. Which is why sometimes it would work, and sometimes it wouldn't, but if I copied it to another location (which reset to the default build action), it would work.

Sorry about the moderately paniced harrassment.

Have a great weekend.

Posted 11 years ago by Actipro Software Support - Cleveland, OH, USA
Ha, I was about to ask if you were sure your files were included as either Page or Resource. Glad that was it. Little things can be tough to sort out sometimes can't they? :)

Actipro Software Support
Information The latest build of this product (2018.1 build 0674) was released 1 month ago, which was after the last post in this thread.

Add a Comment

Please log in to a validated account to post comments.