Search code examples
c#wpfxamldynamicresource

How do I duplicate a resource reference in code behind in WPF?


In my application, I have a color resources. I have one element that uses that color as a dynamic resource in xaml.

  <Window x:Class="ResourcePlay.MainWindow"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          Title="MainWindow" Height="350" Width="425">
     <Window.Resources>
        <Color x:Key="MyColor">Red</Color>
     </Window.Resources>
     <Grid>
        <Rectangle VerticalAlignment="Top" Width="80" Height="80" Margin="10">
           <Rectangle.Fill>
              <SolidColorBrush x:Name="TopBrush" Color="{DynamicResource MyColor}"/>
           </Rectangle.Fill>
        </Rectangle>
        <Rectangle VerticalAlignment="Bottom" Width="80" Height="80" Margin="10">
           <Rectangle.Fill>
              <SolidColorBrush x:Name="BottomBrush"/>
           </Rectangle.Fill>
        </Rectangle>
     </Grid>
  </Window>

In the code, I want to duplicate this resource reference.

  using System.Windows;
  using System.Windows.Media;

  namespace ResourcePlay {
     public partial class MainWindow : Window {
        public MainWindow() {
           InitializeComponent();

           // I want to copy the resource reference, not the color.
           BottomBrush.Color = TopBrush.Color;

           // I'd really rather do something like this.
           var reference = TopBrush.GetResourceReference(SolidColorBrush.ColorProperty);
           BottomBrush.SetResourceReference(reference);

           // I want this to change the colors of both elements
           Resources["MyColor"] = Colors.Green;
        }
     }
  }

However, SetResourceReference only works for FrameworkElements or FrameworkContentElements. SolidColorBrush is just a Freezable. Also, I have no idea how to get a resource reference in code behind.

Is there a way to do this in WPF so that both of the colors change at the same time? In my real application, the problem isn't quite so simple, so I can't just add a second DynamicResource in xaml.


Solution

  • Il Vic suggested using reflection. Expanding on that, I was able to build some extension methods for DependencyObject that do what I want. I don't really like using reflection in code, and if someone else knows a better way to implement this, I'd love to see it. At least this will be helpful whenever I'm trying to debug DynamicResources from code behind.

      public static class DependencyObjectExtensions
      {
         public static object GetDynamicResourceKey(this DependencyObject obj, DependencyProperty prop)
         {
            // get the value entry from the depencency object for the specified dependency property
            var dependencyObject = typeof(DependencyObject);
            var dependencyObject_LookupEntry = dependencyObject.GetMethod("LookupEntry", BindingFlags.NonPublic | BindingFlags.Instance);
            var entryIndex = dependencyObject_LookupEntry.Invoke(obj, new object[] { prop.GlobalIndex });
            var effectiveValueEntry_GetValueEntry = dependencyObject.GetMethod("GetValueEntry", BindingFlags.NonPublic | BindingFlags.Instance);
            var valueEntry = effectiveValueEntry_GetValueEntry.Invoke(obj, new object[] { entryIndex, prop, null, 0x10 });
    
            // look inside the value entry to find the ModifiedValue object
            var effectiveValueEntry = valueEntry.GetType();
            var effectiveValueEntry_Value = effectiveValueEntry.GetProperty("Value", BindingFlags.Instance | BindingFlags.NonPublic);
            var effectiveValueEntry_Value_Getter = effectiveValueEntry_Value.GetGetMethod(nonPublic: true);
            var rawEntry = effectiveValueEntry_Value_Getter.Invoke(valueEntry, new object[0]);
    
            // look inside the ModifiedValue object to find the ResourceReference
            var modifiedValue = rawEntry.GetType();
            var modifiedValue_BaseValue = modifiedValue.GetProperty("BaseValue", BindingFlags.Instance | BindingFlags.NonPublic);
            var modifiedValue_BaseValue_Getter = modifiedValue_BaseValue.GetGetMethod(nonPublic: true);
            var resourceReferenceValue = modifiedValue_BaseValue_Getter.Invoke(rawEntry, new object[0]);
    
            // check the ResourceReference for the original ResourceKey
            var resourceReference = resourceReferenceValue.GetType();
            var resourceReference_resourceKey = resourceReference.GetField("_resourceKey", BindingFlags.NonPublic | BindingFlags.Instance);
            var resourceKey = resourceReference_resourceKey.GetValue(resourceReferenceValue);
    
            return resourceKey;
         }
    
         public static void SetDynamicResourceKey(this DependencyObject obj, DependencyProperty prop, object resourceKey)
         {
            var dynamicResource = new DynamicResourceExtension(resourceKey);
            var resourceReferenceExpression = dynamicResource.ProvideValue(null);
            obj.SetValue(prop, resourceReferenceExpression);
         }
      }
    

    The second method uses DynamicResourceExtension to avoid some nastiness with Activator, but the first method feels incredibly brittle.

    I can use these methods in my original example as follows:

      public MainWindow() {
         InitializeComponent();
    
         var key = TopBrush.GetDynamicResourceKey(SolidColorBrush.ColorProperty);
         BottomBrush.SetDynamicResourceKey(SolidColorBrush.ColorProperty, key);
    
         Resources["MyColor"] = Colors.Green;
      }
    

    This will work for any DependencyProperty, provided it is set to a DynamicResource when we try to get the resource key. A little more finesse would be needed for production code.