Best way to cache theme resources to avoid dynamic resource lookups

WPF Studio, Themes, and Shared Library for WPF Forum

Posted 4 years ago by Eric Litovsky
Version: 15.1.0622
Avatar

We have an existing application which is extensively styled throughout and utuilizes a common resource dictionary which contains keyed brushes defined as static resources.

We would like to bind the color values of our solid color resource brushes to the colors of brushes in your theme libraries.  We need to change the color pallate of our application to reflect the colors of an ActiPro theme.

I've tried sub-classing a brush (which is not supported by the framework), dynamic markup extensions and standard markup extensions. All have failed due to the delayed nature of the dynamic resource binding strategy.

The only way I have been able bind the color value of our static brushes to the color of a brush in your theme library is through an attached property I registered as "SourceBrush" (Example below).  

Problem:  We have thousands of styled elements being rendered and updated.  Having each one look up the Dynamic Resource of the designated brush key is straining our system.

Question:  Is there a better (more efficient) way to implement just the color pallate of your theme(s) using the static brushes defined in our resource dictionary?  

Example Follows:

<SolidColorBrush x:Key="GridBackground"
                 PresentationOptions:Freeze="True"
                 extensions:ThemeProperties.SourceBrush="{DynamicResource 
                   {x:Static themes:AssetResourceKeys.ControlBackgroundNormalBrushKey}}" />
 

 

 public class ThemeProperties : DependencyObject
  {
    public static readonly DependencyProperty
        SourceBrushProperty =
        DependencyProperty.RegisterAttached(
              "SourceBrush", typeof(SolidColorBrush), typeof(ThemeProperties),
        new PropertyMetadata(CallBackWhenPropertyIsChanged));

    public static SolidColorBrush GetSourceBrush(
        DependencyObject d)
    {
      return (SolidColorBrush)d.GetValue(SourceBrushProperty);
    }

    public static void SetSourceBrush(
        DependencyObject d, SolidColorBrush value)
    {
      d.SetValue(SolidColorBrush.ColorProperty, value.Color);
    }

    private static void CallBackWhenPropertyIsChanged(
     object sender,
     DependencyPropertyChangedEventArgs args)
    {
      var attachedObject = sender as SolidColorBrush;
      if (attachedObject != null)
      {
        attachedObject.Color = ((SolidColorBrush) args.NewValue).Color;
      }     
    }
  }

[Modified 4 years ago]

Comments (6)

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

Hi Eric,

Other than what you are doing, there isn't really a better way to dynamically build brushes. 

I would actually suggest that you statically build them though.  Since our brushes would only ever change when you change builds of ours, you could write some routine that enumerates all our brushes that you use and creates a color table.  Then output your own XAML resource dictionary that contains your own app's brushes based on that color table.  That XAML resource dictionary could be directly included in your app's Resources and it wouldn't have any DynamicResource references in it then.


Actipro Software Support

Posted 4 years ago by Eric Litovsky
Avatar

Actually tried this approach as well, but was not able to get the correct brushes from code.  When I invoke Application.Current.TryFindResource(AssetResourceKeys.xxx) null is returned, when I use the code below, I get brushes which do not match the current theme, the colors don't match.  Is there a way to explicitly get a brush from your theme library based on the resource key and theme name?

If you could provide a better example or sample project based on the reply you provided above, I would definitely appreciate it.

<SolidColorBrush x:Key="GridBackground"
                     PresentationOptions:Freeze="True"
                     Color="{extensions:DynamicResourceWithConverter 
                                {x:Static themes:AssetResourceKeys.ListRowAlternateBackgroundNormalBrushKey}, 
                                 Converter={StaticResource BrushToColorConverter} }" />

 

[MarkupExtensionReturnType(typeof(Color))] 
  public class DynamicResourceWithConverterExtension : DynamicResourceExtension, INotifyPropertyChanged
  {

    public DynamicResourceWithConverterExtension()
    {
    }

    public DynamicResourceWithConverterExtension(object resourceKey)
      : base(resourceKey)
    {
    }

    public IValueConverter Converter { get; set; }
    public object ConverterParameter { get; set; }
    public object CachedValue { get; set; }
  
    public override object ProvideValue(IServiceProvider provider)
    {

      if (CachedValue != null)
        return CachedValue;

      object value = base.ProvideValue(provider);
      Expression e;

      if (value is Expression)
          value = Application.Current.TryFindResource(this.ResourceKey);

      if (value != this && Converter != null)
      {
        Type targetType = null;
        var target = (IProvideValueTarget)provider.GetService(typeof(IProvideValueTarget));
        if (target != null)
        {
          DependencyProperty targetDp = target.TargetProperty as DependencyProperty;
          if (targetDp != null)
          {
            targetType = targetDp.PropertyType;
          }
        }
        if (targetType != null)
          CachedValue = Converter.Convert(value, targetType, ConverterParameter, CultureInfo.CurrentCulture);
      }
      else
      {
        CachedValue = value;
      }

      return CachedValue;
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion
  }

[Modified 4 years ago]

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

Hello,

My guess is that you didn't run any ThemeManager initialization yet then.  When I paste a line like this at the bottom of our WPF Sample Browser's App.OnStartup (after all the ThemeManager code ran), I get a valid brush result:

var brush = Application.Current.TryFindResource(AssetResourceKeys.ControlBackgroundNormalBrushKey);


Actipro Software Support

Posted 4 years ago by Eric Litovsky
Avatar

This is the problem I'm seeing, with the exact same code:

 

I have a watch set up for ThemeManager.CurrentTheme and it indicates "MetroDark"

var brush = Application.Current.TryFindResource(AssetResourceKeys.ControlBackgroundNormalBrushKey);

At first, the brush value returned is: #FFF0F0F0 which does not match the ControlBackgroundNormalBrushKey in the Metro Dark Theme (#FF252526).

In the attached property, I can see the same brush resource first return the #FFF0F0F0 color and after a a few seconds, change to return the correct brush color (#FF252526).  It looks like there is a timing issue between when the ThemeManager.CurrentTheme is set and when the actual resources of that theme starts being returned.

I guess this could be caused by the fact that the dynamic markup extension is defined and runs in a library referenced by the main client application.

It does seem like certain elements are being colored correctly, but it all seems to depend on when they are loaded.

I'm at a loss as to how to resolve this timing issue.  

Posted 4 years ago by Eric Litovsky
Avatar

I was able to resolve this issue internally by changing the way order and method by which our internal resource files were loaded.

On a related note, we would like to bind our internal color resources to colors of theme brushes.

<Color x:Key="MyColorKey">#VALUE</Color>

 Where #VALUE can be a color from a theme brush.

I know this is may not be possible because Color is a struct here and no extension or attached properties are supported, but if you have a creative suggestion as to how to approach this, I would also appreciate it.

 

Thanks again for all your help.

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

Hi Eric,

I'm glad you got it working.  I'm not sure WPF really has a way to dynamically update the Colors in XAML format at least.  You could make a custom class that inherits ResourceDictionary and adds to it dynamically upon creation though, via C#/VB.

Or you could do what we suggested a bit further back where you just make some sort of generator app that outputs a resource dictionary to include in your project based on the contents of the Actipro themes.  Then you only refresh that when you upgrade to new builds of the Actipro WPF controls.  That way you do the work up front at coding time and not dynamically during app execution.


Actipro Software Support

The latest build of this product (v2019.1 build 0683) was released 2 months ago, which was after the last post in this thread.

Add Comment

Please log in to a validated account to post comments.