Как мне продублировать ссылку на ресурс в коде позади в WPF?

В моем приложении есть цветные ресурсы. У меня есть один элемент, который использует этот цвет как динамический ресурс в 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>

В коде я хочу продублировать эту ссылку на ресурс.

  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;
        }
     }
  }

Однако SetResourceReference работает только для FrameworkElements или FrameworkContentElements. SolidColorBrush - это просто Freezable. Кроме того, я понятия не имею, как получить ссылку на ресурс в исходном коде.

Есть ли способ сделать это в WPF, чтобы оба цвета менялись одновременно? В моем реальном приложении проблема не так проста, поэтому я не могу просто добавить второй DynamicResource в xaml.


person benjamin.popp    schedule 30.01.2015    source источник
comment
Пожалуйста, подробно объясните, почему вы не просто объявляете SolidColorBrush как ресурс, а затем используете его для свойств Fill тех элементов, где вы этого хотите.   -  person Peter Duniho    schedule 31.01.2015
comment
@PeterDuniho, SolidColorBrush - это прокси для настраиваемого подкласса Freezable, который я использую в своем приложении. Реальный объект параметризован для внедрения зависимостей, поэтому я не могу создать объект в xaml.   -  person benjamin.popp    schedule 02.02.2015
comment
Понятия не имею, почему кто-то проголосовал против проблемы, поскольку это был мой точный вопрос. Эта ветка предоставила мне решение.   -  person TravisWhidden    schedule 10.08.2017


Ответы (2)


Иль Вик предложил использовать рефлексию. Расширяя это, я смог создать несколько методов расширения для DependencyObject, которые делают то, что я хочу. Мне не очень нравится использовать отражение в коде, и если кто-то знает лучший способ реализовать это, я бы с удовольствием его увидел. По крайней мере, это будет полезно всякий раз, когда я пытаюсь отлаживать DynamicResources из исходного кода.

  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);
     }
  }

Второй метод использует DynamicResourceExtension, чтобы избежать неприятностей с Activator, но первый метод кажется невероятно хрупким.

Я могу использовать эти методы в моем исходном примере следующим образом:

  public MainWindow() {
     InitializeComponent();

     var key = TopBrush.GetDynamicResourceKey(SolidColorBrush.ColorProperty);
     BottomBrush.SetDynamicResourceKey(SolidColorBrush.ColorProperty, key);

     Resources["MyColor"] = Colors.Green;
  }

Это будет работать для любого DependencyProperty при условии, что для него установлено значение DynamicResource, когда мы пытаемся получить ключ ресурса. Для производственного кода потребуется немного больше изящества.

person benjamin.popp    schedule 02.02.2015
comment
Мне тоже не нравится массовое отражение в моем коде, но вы очень хорошо поработали в стандартных сборках! Отличная работа! - person Il Vic; 02.02.2015
comment
Это сработало для меня как чемпион в .net 4.6 - позже это могло вызвать у меня некоторое горе, но сделало именно то, что мне нужно. Спасибо. - person TravisWhidden; 10.08.2017

На самом деле вам нужен внутренний объект с именем ResourceReferenceExpression. Он используется в DynamicResourceExtention.

Это код, который вы можете использовать:

public MainWindow()
{
    InitializeComponent();

    BottomBrush.SetValue(SolidColorBrush.ColorProperty,
        Activator.CreateInstance(Type.GetType("System.Windows.ResourceReferenceExpression, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"), "MyColor"));

    Resources["MyColor"] = Colors.Green;
}

Используйте его осторожно! Поскольку ResourceReferenceExpression является внутренним, может быть причина (возможно, неправильное использование этого объекта может привести к утечке памяти).

person Il Vic    schedule 30.01.2015
comment
Это почти то, что я ищу. Есть ли способ получить ResourceReferenceExpression, который использует TopBrush, а затем использовать то же выражение (или копию) для нижнего? Я бы предпочел не жестко кодировать MyColor для второй кисти - я хочу, чтобы он использовал то, что использует первая кисть. - person benjamin.popp; 30.01.2015
comment
Я полагаю, что нет возможности получить ResourceReferenceExpression: мы говорим о внутреннем объекте, а не о публичном. Наверное, стоит изменить подход к проблеме. Может быть, вы можете рассмотреть возможность использования SolidColorBrush в качестве ресурса (как предлагает Питер). Или, может быть, вы можете установить цвет обеих кистей в своем XAML, а не в своем коде. - person Il Vic; 31.01.2015