WPF: почему моя привязка расширения разметки не работает?

Рабочий пример с «Привязкой»:

У меня есть UserControl, который я использую в своем MainWindow следующим образом:

<userControls:NoMarkupControl/>

ViewModel моего MainWindow содержит это свойство:

private string _exampleText = "example";
public string ExampleText
{
   get { return _exampleText; }
   set
   {
      _exampleText = value;
      OnPropertyChanged();
   }
}

внутри UserControl я привязываю свою ViewModel к этому свойству:

<TextBlock Text = "{Binding ExampleText}"/>

в результате «пример» отображается при запуске приложения. Все работает.

Нерабочий пример с пользовательским расширением разметки:

Теперь у меня есть MarkupExtension:

public class ExampleTextExtension : MarkupExtension
{
    private static readonly List<DependencyProperty> StorageProperties = new List<DependencyProperty>();

    private readonly object _parameter;

    private DependencyProperty _dependencyProperty;

    public ExampleTextExtension(object parameter)
    {
        _parameter = parameter;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        DependencyObject targetObject;
        if (target?.TargetObject is DependencyObject dependencyObject &&
            target.TargetProperty is DependencyProperty)
        {
            targetObject = dependencyObject;
        }
        else
        {
            return this;
        }

        _dependencyProperty = SetUnusedStorageProperty(targetObject, _parameter);

        return GetLocalizedText((string)targetObject.GetValue(_dependencyProperty));
    }

    private static string GetLocalizedText(string text)
    {
        return text == null ? null : $"markup: {text}";
    }

    private static DependencyProperty SetUnusedStorageProperty(DependencyObject obj, object value)
    {
        var property = StorageProperties.FirstOrDefault(p => obj.ReadLocalValue(p) == DependencyProperty.UnsetValue);

        if (property == null)
        {
            property = DependencyProperty.RegisterAttached("Storage" + StorageProperties.Count, typeof(object), typeof(ExampleTextExtension), new PropertyMetadata());
            StorageProperties.Add(property);
        }

        if (value is MarkupExtension markupExtension)
        {
            var resolvedValue = markupExtension.ProvideValue(new ServiceProvider(obj, property));
            obj.SetValue(property, resolvedValue);
        }
        else
        {
            obj.SetValue(property, value);
        }

        return property;
    }

    private class ServiceProvider : IServiceProvider, IProvideValueTarget
    {
        public object TargetObject { get; }
        public object TargetProperty { get; }

        public ServiceProvider(object targetObject, object targetProperty)
        {
            TargetObject = targetObject;
            TargetProperty = targetProperty;
        }

        public object GetService(Type serviceType)
        {
            return serviceType.IsInstanceOfType(this) ? this : null;
        }
    }
}

Опять же, у меня есть UserControl, который я использую в своем MainWindow следующим образом:

<userControls:MarkupControl/>

ViewModel моего MainWindow остается таким же, как указано выше.

внутри UserControl я привязываюсь к своему свойству TextBlock Text следующим образом:

<TextBlock Text = "{markupExtensions:ExampleText {Binding ExampleText}}"/>

в результате мой UserControl ничего не отображает. Я бы ожидал, что отобразится «разметка: пример»

Привязка как-то не работает в этом случае.

Кто-нибудь знает, как это исправить?

Дополнительная информация:

он работает при таком использовании (свойство зависимости MarkupText создается в пользовательском элементе управления):

<userControls:MarkupControl MarkupText = {markupExtensions:ExampleText {Binding ExampleText}}/>

<TextBlock Text = "{Binding Text, ElementName=MarkupControl}"/>

Зачем расширение разметки? Вместо динамического ресурса или просто свойства в модели представления?

Andy 17.11.2022 17:50

Вы должны установить переданное в Binding свойство зависимости, чтобы активировать его. Это механизм привязки, который фактически выполняет всю работу по связыванию целевого свойства с исходным свойством. Механизм привязки является частью инфраструктуры свойств зависимостей. Вот почему цель Binding должна быть свойством зависимости. Вам необходимо создать промежуточное свойство зависимости для разрешения Binding. Обработайте события Binding SourceUpdated и TargetUpdated, чтобы получить обновленное значение. Затем обработайте/обработайте его и отправьте в цель вашего пользовательского расширения разметки.

BionicCode 17.11.2022 23:29

Чтобы прикрепить Binding, ваше промежуточное свойство должно быть определено DependencyObject. Это означает, что вам нужно создать специальный класс для разрешения привязки.

BionicCode 17.11.2022 23:56

@Andy Я создал это расширение разметки, чтобы показать, что не работает, мое настоящее расширение разметки обрабатывает какое-то изменение языка. Я мог бы сделать это и на виртуальной машине, но я думаю, что расширение разметки делает его чище и (если работает) проще в использовании.

Sebastian 18.11.2022 08:54

@BionicCode Я не уверен, что понимаю тебя. Я думал, что уже использую свойство зависимости: property = DependencyProperty.RegisterAttached("Storage" + StorageProperties.Count, typeof(object), typeof(ExampleTextExtension), new PropertyMetadata()); и здесь я связываю dp с объектом зависимости: var resolvedValue = markupExtension.ProvideValue(new ServiceProvider(obj, property)); obj.SetValue(property, resolvedValue); можете ли вы опубликовать пример или попытаться указать, что вы имеете в виду, пожалуйста? Привязка в основном работает, но не в случае, указанном в моем вопросе.

Sebastian 18.11.2022 09:01

Под «каким-то изменением языка» вы буквально подразумеваете увидеть все на французском, немецком или английском языке - локализацию? Динамический ресурс и объединение словаря ресурсов для каждого языка — хороший способ сделать это.

Andy 18.11.2022 09:44

@ Энди, да на твой вопрос, но нет на твое предложение. все тексты хранятся в базе данных и доступны через сервис...

Sebastian 18.11.2022 13:31

Почему это означает, что вы не можете объединить их как ресурсы? Вы можете создать словарь ресурсов в коде или установить уже объединенные значения, или объединить плоский файл, или объединить строку и т. д. Это расширение разметки не кажется мне хорошей идеей. Я думаю, проблема в том, что у вас есть дп в одном классе, а вы привязываете его к другому.

Andy 18.11.2022 19:55

Другим вариантом является безликий элемент управления, предоставляющий dp string[] или наблюдаемый словарь строк. Вы можете использовать обычную привязку и загружать ее так, как вам нравится.

Andy 18.11.2022 20:23

@ Энди, я не уверен, как мне получить тексты из этой базы данных (принадлежит третьей стороне, у меня есть доступ только через SDK), а затем использовать ее в качестве ресурса. Можете ли вы объяснить этот подход немного дальше?

Sebastian 24.11.2022 17:37

Если вы вообще можете их прочитать, то вы можете прочитать и сохранить как обычный некомпилированный словарь ресурсов. Которые затем могут быть объединены в любое время. Хотя что я объясняю? То, как вы получаете эти строки, кажется, что это может иметь значение в той или иной степени. В любом случае. У вас тут странный дизайн.

Andy 24.11.2022 21:42

@Энди, а, теперь я понял, что ты имеешь в виду... не думал об этом, спасибо

Sebastian 28.11.2022 08:50
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
12
83
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Во-вторых, у вас проблема со временем. При первом вызове расширения Binding не инициализируется должным образом: оно не предоставляет окончательное значение Binding.Source. Кроме того, ваша текущая реализация не поддерживает изменение свойств. Чтобы исправить это, вам нужно будет отслеживать обновления Binding.Target, когда значение отправляется из Binding.Source (по умолчанию BindingMode.OneWay). Вы можете добиться этого, прослушав событие Binding.TargetUpdated (как указано в моем предыдущем комментарии) или зарегистрировав обработчик изменения свойства с присоединенным свойством (рекомендуется).
Чтобы поддерживать двустороннюю привязку, вам также необходимо отслеживать целевое свойство (свойство, которому назначен ваш MarkupExtension).

Исправленная и улучшенная версия может выглядеть следующим образом:

public class ExampleTextExtension : MarkupExtension
{
  private static DependencyProperty ResolvedBindingSourceValueProperty = DependencyProperty.RegisterAttached(
    "ResolvedBindingSourceValue",
    typeof(object),
    typeof(ExampleTextExtension),
    new PropertyMetadata(default(object), OnResolvedBindingSourceValueChanged));  

  // Use attached property to store the target object
  // for reference from a static context without dealing with class level members that are shared between instances.
  private static DependencyProperty TargetPropertyProperty = DependencyProperty.RegisterAttached(
    "TargetProperty",
    typeof(DependencyProperty),
    typeof(ExampleTextExtension),
    new PropertyMetadata(default));

  private Binding Binding { get; }

  // Accept BindingBase to support MultiBinding etc.
  public ExampleTextExtension(Binding binding)
  {
    this.Binding = binding;
  }

  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    var provideValueTargetService = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
    if (provideValueTargetService?.TargetObject is not DependencyObject targetObject
      || provideValueTargetService?.TargetProperty is not DependencyProperty targetProperty)
    {
      return this;
    }

    targetObject.SetValue(ExampleTextExtension.TargetPropertyProperty, targetProperty);
    AttachBinding(targetObject);
    return string.Empty;
  }

  private static string GetLocalizedText(string text) 
    => String.IsNullOrWhiteSpace(text) 
      ? string.Empty 
      : $"markup: {text}";

  // By now, only supports OneWay binding
  private void AttachBinding(DependencyObject targetObject)
  {
    switch (this.Binding.Mode)
    {
      case BindingMode.OneWay:
      case BindingMode.Default:
        HandleOneWayBinding(targetObject); break;
      default: throw new NotSupportedException();
    }
  }

  private void HandleOneWayBinding(DependencyObject targetObject)
  {
    BindingOperations.SetBinding(targetObject, ExampleTextExtension.ResolvedBindingSourceValueProperty, this.Binding);
  }

  // Property changed handler to update the target of this extension
  // with the localized value
  private static void OnResolvedBindingSourceValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    string localizedText = GetLocalizedText(e.NewValue as string);
    var targetProperty = d.GetValue(ExampleTextExtension.TargetPropertyProperty) as DependencyProperty;
    d.SetValue(targetProperty, localizedText);
  }
}

Примечания

Есть лучшие решения для введения локализации без ущерба для общего синтаксиса или устаревшего кода. Например, введение этого MarkupExtension в существующий код нарушит этот код, поскольку все соответствующие привязки данных (C# и XAML) должны быть изменены.

Наиболее распространенным подходом является использование вспомогательных сборок и локализованных ресурсов. Вместо преобразования текстовых значений во время привязки данных вы должны локализовать источник значения напрямую (чтобы Binding передавал уже локализованные значения).
Другими словами, убедитесь, что источник данных локализован. Пусть источник привязки предоставляет текст, извлекая его из локализованного репозитория.

Спасибо. часть OnResolvedBindingSourceValueChanged была тем, чего не хватало и чего я не получил. так что это был ответ, который я искал...

Sebastian 24.11.2022 17:17

у меня уже была остальная часть (упрощение) вашей реализации, но я добавил отслеживание через коллекцию после того, как прочитал эту статью: singulink.com/codeindex/post/… где автор заявил: хранить свои значения в своей цели элемент, и мы не регистрируем без необходимости дополнительные прикрепленные свойства или утечку памяти, поскольку элементы пользовательского интерфейса создаются и уничтожаются снова и снова, и регистрируется все больше и больше свойств, можете ли вы объяснить, почему это не проблема в моем случае?

Sebastian 24.11.2022 17:19

в реализации, которая у меня была раньше (примерно так: stackoverflow.com/a/37667896), вместо привязки использовалась bindingbase, и возвращаемое значение в ProvideValue также было другим. можешь объяснить разницу?

Sebastian 24.11.2022 17:27

я посмотрю на то, что вы предложили. возможно ли изменить язык во время выполнения с этими сателлитными сборками? это обязательно в моем приложении.

Sebastian 24.11.2022 17:29

Вам не нужна часть коллекции. Автор использовал его, чтобы разрешить передачу неограниченного количества аргументов расширению разметки. Он создает выделенное свойство для каждого аргумента. Я предполагаю, что гораздо лучшим решением было бы создать одно прикрепленное свойство и сохранить аргументы в списке. Хранение такого количества свойств зависимостей в статической коллекции эффективно создает ненужные «утечки памяти», поскольку сборщик мусора не может собирать статические ссылки или ссылки, на которые ссылаются статические ссылки. Я предполагаю, что он был в курсе и поэтому пытался повторно использовать как можно больше свойств.

BionicCode 24.11.2022 19:15

Вместо этого он должен был хранить список значений в одном прикрепленном свойстве. Поскольку ваше расширение принимает только одно значение для каждого экземпляра, вам определенно не нужна часть коллекции. Одно прикрепленное свойство для разрешения Binding делает свое дело. ==

BionicCode 24.11.2022 19:16

BindingBase — это базовый класс Binding, MultiBinding и PriorityyBinding. Чтобы обеспечить совместимость расширения со всеми типами привязок, вы должны написать код, ориентированный на общий базовый класс (BindingBase). "и доходность в ProvideValue также была другой. Вы можете объяснить разницу?" - что вы имеете в виду под "отдача тоже была другой" по сравнению с чем? ===

BionicCode 24.11.2022 19:16
Разработка локализуемых приложений , Локализация приложения WPF и многие другие ресурсы в Интернете.
BionicCode 24.11.2022 19:16

большое спасибо за ваши объяснения ... то, что я имел в виду под «возвратом было другим», было возвратом в функции ProvideValue. в решении, которое я связал return param1InnerBinding.ProvideValue(serviceProvider); // return binding to Param1.SomeInnerProperty, было возвращено. так как вы просто возвращаете string.empty, я не понимаю, для чего используется возврат ProvideValue, потому что в вашей реализации это кажется ненужным...

Sebastian 28.11.2022 09:05

Другие вопросы по теме