Строковый формат с расширением разметки

Я пытаюсь сделать string.Format доступным как удобную функцию в WPF, так что различные текстовые части можно объединить в чистом XAML, без шаблона в коде. Основная проблема заключается в поддержке случаев, когда аргументы функции поступают из других вложенных расширений разметки (например, Binding).

На самом деле есть функция, которая очень близка к тому, что мне нужно: MultiBinding. К сожалению, он может принимать только привязки, но не другие динамические типы контента, такие как DynamicResource s.

Если все мои источники данных были привязками, я мог бы использовать разметку следующим образом:

<TextBlock>
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource StringFormatConverter}">
            <Binding Path="FormatString"/>
            <Binding Path="Arg0"/>
            <Binding Path="Arg1"/>
            <!-- ... -->
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

с очевидной реализацией StringFormatConveter.

Я попытался реализовать собственное расширение разметки, чтобы синтаксис был таким:

<TextBlock>
    <TextBlock.Text>
        <l:StringFormat Format="{Binding FormatString}">
            <DynamicResource ResourceKey="ARG0ID"/>
            <Binding Path="Arg1"/>
            <StaticResource ResourceKey="ARG2ID"/>
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

или, может быть, просто

<TextBlock Text="{l:StringFormat {Binding FormatString},
                  arg0={DynamicResource ARG0ID},
                  arg1={Binding Arg2},
                  arg2='literal string', ...}"/>

Но я застрял в реализации ProvideValue(IServiceProvider serviceProvider) для аргумента, являющегося другим расширением разметки.

Большинство примеров в Интернете довольно тривиальны: они либо вообще не используют serviceProvider, либо запрос IProvideValueTarget, который (в основном) говорит, что свойство зависимостей является целью расширения разметки. В любом случае код знает значение, которое должно быть предоставлено во время вызова ProvideValue. Однако ProvideValue будет вызываться только один раз (за исключением шаблонов, которые являются отдельной историей), поэтому следует использовать другую стратегию, если фактическая значение не является постоянным (например, для Binding и т.д.).

Я просмотрел реализацию Binding в Reflector, его метод ProvideValue фактически возвращает не реальный целевой объект, а экземпляр класса System.Windows.Data.BindingExpression, который, кажется, выполняет всю реальную работу. То же самое касается DynamicResource: он просто возвращает экземпляр System.Windows.ResourceReferenceExpression, который заботится о подписке на (внутренний) InheritanceContextChanged и недействительности значения, когда это необходимо. То, что я, однако, не мог понять, просматривая код, выглядит следующим образом:

  • Как получается, что объект типа BindingExpression/ResourceReferenceExpression не обрабатывается как "есть", но запрашивается базовое значение?
  • Как MultiBindingExpression знает, что значения базовых привязок изменились, поэтому он также должен лишить его значения?

Я действительно нашел реализацию библиотеки расширений разметки, которая утверждает, что поддерживает конкатенирование строк (что идеально подходит для моего использования) (проект, code, реализация конкатенации, полагаясь на другой код), но он, похоже, поддерживает вложенные расширения только типов библиотек (т.е. внутри не существует гнездо ваниль Binding).

Есть ли способ реализовать синтаксис, представленный в начале вопроса? Это поддерживаемый сценарий, или это можно сделать только изнутри инфраструктуры WPF (поскольку System.Windows.Expression имеет внутренний конструктор)?


На самом деле у меня есть реализация необходимой семантики, используя пользовательский невидимый вспомогательный элемент пользовательского интерфейса:

<l:FormatHelper x:Name="h1" Format="{DynamicResource FORMAT_ID'">
    <l:FormatArgument Value="{Binding Data1}"/>
    <l:FormatArgument Value="{StaticResource Data2}"/>
</l:FormatHelper>
<TextBlock Text="{Binding Value, ElementName=h1}"/>

(где FormatHelper отслеживает его дочерние элементы и обновляет их свойства зависимостей и сохраняет обновленный результат в Value), но этот синтаксис кажется уродливым, и я хочу избавиться от вспомогательных элементов в визуальное дерево.


Конечная цель состоит в том, чтобы облегчить перевод: строки UI, такие как "15 секунд до взрыва", естественно представлены в виде локализуемого формата "{0} до взрыва" (который переходит в ResourceDictionary и будет заменен при изменении языка ) и Binding к свойству зависимости VM, представляющему время.


Обновить отчет. Я попытался реализовать расширение разметки самостоятельно со всей информацией, которую я мог найти в Интернете. Полная реализация здесь ([1], [2], [3]), вот основная часть:

var result = new MultiBinding()
{
    Converter = new StringFormatConverter(),
    Mode = BindingMode.OneWay
};

foreach (var v in values)
{
    if (v is MarkupExtension)
    {
        var b = v as Binding;
        if (b != null)
        {
            result.Bindings.Add(b);
            continue;
        }

        var bb = v as BindingBase;
        if (bb != null)
        {
            targetObjFE.SetBinding(AddBindingTo(targetObjFE, result), bb);
            continue;
        }
    }

    if (v is System.Windows.Expression)
    {
        DynamicResourceExtension mex = null;
        // didn't find other way to check for dynamic resource
        try
        {
            // rrc is a new ResourceReferenceExpressionConverter();
            mex = (MarkupExtension)rrc.ConvertTo(v, typeof(MarkupExtension))
                as DynamicResourceExtension;
        }
        catch (Exception)
        {
        }
        if (mex != null)
        {
            targetObjFE.SetResourceReference(
                    AddBindingTo(targetObjFE, result),
                    mex.ResourceKey);
            continue;
        }
    }

    // fallback
    result.Bindings.Add(
        new Binding() { Mode = BindingMode.OneWay, Source = v });
}

return result.ProvideValue(serviceProvider);

Это, похоже, работает с привязками вложений и динамическими ресурсами, но с треском проваливается, пытаясь вложить его в себя, так как в этом случае targetObj, полученный из IProvideValueTarget, null. Я попытался обойти это, объединив вложенные привязки во внешний ([1a], [2a]) (добавлено многосвязное разлитие во внешнее связывание), возможно, это работало бы с вложенными многосвязными и расширением формата, но все еще не удается с вложенными динамическими ресурсами.

Интересно, что при развёртывании разного рода расширений разметки я получаю Binding и MultiBinding во внешнем расширении, но ResourceReferenceExpression вместо DynamicResourceExtension. Интересно, почему это непоследовательно (и как Binding реконструирован из BindingExpression).


Обновить отчет: к сожалению, идеи, приведенные в ответах, не привели к решению проблемы. Возможно, это доказывает, что расширения разметки, будучи достаточно мощным и универсальным инструментом, нуждаются в большем внимании со стороны команды WPF.

В любом случае я благодарю всех, кто принимал участие в обсуждении. Частичные решения, которые были представлены, достаточно сложны, чтобы заслуживать большего внимания.


Обновить отчет: похоже, нет хорошего решения с расширениями разметки или, по крайней мере, уровень знаний WPF, необходимых для его создания, слишком глубок, чтобы быть практичным.

Тем не менее, у @adabyron появилась идея улучшения, которая помогает скрыть вспомогательные элементы в элементе хоста (цена этого, однако, подклассифицирует хост). Я постараюсь выяснить, возможно ли избавиться от подкласса (используя поведение, которое захватывает хост LogicalChildren и добавляет к нему вспомогательные элементы, в основе которого лежит старая версия того же ответа).

Ответ 1

Посмотрите, работает ли для вас следующее. Я принял тестовый пример, который вы предложили в комментарии, и немного расширил его, чтобы лучше проиллюстрировать механизм. Я предполагаю, что ключ состоит в том, чтобы сохранять гибкость, используя DependencyProperties в контейнере вложенности.

enter image description hereenter image description here

EDIT: я заменил поведение blend подклассом TextBlock. Это добавляет упрощенную связь для DataContext и DynamicResources.

В боковом потоке способ, которым ваш проект использует DynamicResources для введения условий, не является тем, что я бы рекомендовал. Вместо этого попробуйте использовать ViewModel для установки условий и/или использования триггеров.

Xaml:

<UserControl x:Class="WpfApplication1.Controls.ExpiryView" xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
                 xmlns:props="clr-namespace:WpfApplication1.Properties" xmlns:models="clr-namespace:WpfApplication1.Models"
                 xmlns:h="clr-namespace:WpfApplication1.Helpers" xmlns:c="clr-namespace:WpfApplication1.CustomControls"
                 Background="#FCF197" FontFamily="Segoe UI"
                 TextOptions.TextFormattingMode="Display">    <!-- please notice the effect of this on font fuzzyness -->

    <UserControl.DataContext>
        <models:ExpiryViewModel />
    </UserControl.DataContext>
    <UserControl.Resources>
        <system:String x:Key="ShortOrLongDateFormat">{0:d}</system:String>
    </UserControl.Resources>
    <Grid>
        <StackPanel>
            <c:TextBlockComplex VerticalAlignment="Center" HorizontalAlignment="Center">
                <c:TextBlockComplex.Content>
                    <h:StringFormatContainer StringFormat="{x:Static props:Resources.ExpiryDate}">
                        <h:StringFormatContainer.Values>
                            <h:StringFormatContainer Value="{Binding ExpiryDate}" StringFormat="{DynamicResource ShortOrLongDateFormat}" />
                            <h:StringFormatContainer Value="{Binding SecondsToExpiry}" />
                        </h:StringFormatContainer.Values>
                    </h:StringFormatContainer>
                </c:TextBlockComplex.Content>
            </c:TextBlockComplex>
        </StackPanel>
    </Grid>
</UserControl>

TextBlockComplex:

using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using WpfApplication1.Helpers;

namespace WpfApplication1.CustomControls
{
    public class TextBlockComplex : TextBlock
    {
        // Content
        public StringFormatContainer Content { get { return (StringFormatContainer)GetValue(ContentProperty); } set { SetValue(ContentProperty, value); } }
        public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(StringFormatContainer), typeof(TextBlockComplex), new PropertyMetadata(null));

        private static readonly DependencyPropertyDescriptor _dpdValue = DependencyPropertyDescriptor.FromProperty(StringFormatContainer.ValueProperty, typeof(StringFormatContainer));
        private static readonly DependencyPropertyDescriptor _dpdValues = DependencyPropertyDescriptor.FromProperty(StringFormatContainer.ValuesProperty, typeof(StringFormatContainer));
        private static readonly DependencyPropertyDescriptor _dpdStringFormat = DependencyPropertyDescriptor.FromProperty(StringFormatContainer.StringFormatProperty, typeof(StringFormatContainer));
        private static readonly DependencyPropertyDescriptor _dpdContent = DependencyPropertyDescriptor.FromProperty(TextBlockComplex.ContentProperty, typeof(StringFormatContainer));

        private EventHandler _valueChangedHandler;
        private NotifyCollectionChangedEventHandler _valuesChangedHandler;

        protected override IEnumerator LogicalChildren { get { yield return Content; } }

        static TextBlockComplex()
        {
            // take default style from TextBlock
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBlockComplex), new FrameworkPropertyMetadata(typeof(TextBlock)));
        }

        public TextBlockComplex()
        {
            _valueChangedHandler = delegate { AddListeners(this.Content); UpdateText(); };
            _valuesChangedHandler = delegate { AddListeners(this.Content); UpdateText(); };

            this.Loaded += TextBlockComplex_Loaded;
        }

        void TextBlockComplex_Loaded(object sender, RoutedEventArgs e)
        {
            OnContentChanged(this, EventArgs.Empty); // initial call

            _dpdContent.AddValueChanged(this, _valueChangedHandler);
            this.Unloaded += delegate { _dpdContent.RemoveValueChanged(this, _valueChangedHandler); };
        }

        /// <summary>
        /// Reacts to a new topmost StringFormatContainer
        /// </summary>
        private void OnContentChanged(object sender, EventArgs e)
        {
            this.AddLogicalChild(this.Content); // inherits DataContext
            _valueChangedHandler(this, EventArgs.Empty);
        }

        /// <summary>
        /// Updates Text to the Content values
        /// </summary>
        private void UpdateText()
        {
            this.Text = Content.GetValue() as string;
        }

        /// <summary>
        /// Attaches listeners for changes in the Content tree
        /// </summary>
        private void AddListeners(StringFormatContainer cont)
        {
            // in case they have been added before
            RemoveListeners(cont);

            // listen for changes to values collection
            cont.CollectionChanged += _valuesChangedHandler;

            // listen for changes in the bindings of the StringFormatContainer
            _dpdValue.AddValueChanged(cont, _valueChangedHandler);
            _dpdValues.AddValueChanged(cont, _valueChangedHandler);
            _dpdStringFormat.AddValueChanged(cont, _valueChangedHandler);

            // prevent memory leaks
            cont.Unloaded += delegate { RemoveListeners(cont); };

            foreach (var c in cont.Values) AddListeners(c); // recursive
        }

        /// <summary>
        /// Detaches listeners
        /// </summary>
        private void RemoveListeners(StringFormatContainer cont)
        {
            cont.CollectionChanged -= _valuesChangedHandler;

            _dpdValue.RemoveValueChanged(cont, _valueChangedHandler);
            _dpdValues.RemoveValueChanged(cont, _valueChangedHandler);
            _dpdStringFormat.RemoveValueChanged(cont, _valueChangedHandler);
        }
    }
}

StringFormatContainer:

using System.Linq;
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;

namespace WpfApplication1.Helpers
{
    public class StringFormatContainer : FrameworkElement
    {
        // Values
        private static readonly DependencyPropertyKey ValuesPropertyKey = DependencyProperty.RegisterReadOnly("Values", typeof(ObservableCollection<StringFormatContainer>), typeof(StringFormatContainer), new FrameworkPropertyMetadata(new ObservableCollection<StringFormatContainer>()));
        public static readonly DependencyProperty ValuesProperty = ValuesPropertyKey.DependencyProperty;
        public ObservableCollection<StringFormatContainer> Values { get { return (ObservableCollection<StringFormatContainer>)GetValue(ValuesProperty); } }

        // StringFormat
        public static readonly DependencyProperty StringFormatProperty = DependencyProperty.Register("StringFormat", typeof(string), typeof(StringFormatContainer), new PropertyMetadata(default(string)));
        public string StringFormat { get { return (string)GetValue(StringFormatProperty); } set { SetValue(StringFormatProperty, value); } }

        // Value
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(StringFormatContainer), new PropertyMetadata(default(object)));
        public object Value { get { return (object)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } }

        public StringFormatContainer()
            : base()
        {
            SetValue(ValuesPropertyKey, new ObservableCollection<StringFormatContainer>());
            this.Values.CollectionChanged += OnValuesChanged;
        }

        /// <summary>
        /// The implementation of LogicalChildren allows for DataContext propagation.
        /// This way, the DataContext needs only be set on the outermost instance of StringFormatContainer.
        /// </summary>
        void OnValuesChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (var value in e.NewItems)
                    AddLogicalChild(value);
            }
            if (e.OldItems != null)
            {
                foreach (var value in e.OldItems)
                    RemoveLogicalChild(value);
            }
        }

        /// <summary>
        /// Recursive function to piece together the value from the StringFormatContainer hierarchy
        /// </summary>
        public object GetValue()
        {
            object value = null;
            if (this.StringFormat != null)
            {
                // convention: if StringFormat is set, Values take precedence over Value
                if (this.Values.Any())
                    value = string.Format(this.StringFormat, this.Values.Select(v => (object)v.GetValue()).ToArray());
                else if (Value != null)
                    value = string.Format(this.StringFormat, Value);
            }
            else
            {
                // convention: if StringFormat is not set, Value takes precedence over Values
                if (Value != null)
                    value = Value;
                else if (this.Values.Any())
                    value = string.Join(string.Empty, this.Values);
            }
            return value;
        }

        protected override IEnumerator LogicalChildren
        {
            get
            {
                if (Values == null) yield break;
                foreach (var v in Values) yield return v;
            }
        }
    }
}

ExpiryViewModel:

using System;
using System.ComponentModel;

namespace WpfApplication1.Models
{
    public class ExpiryViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private DateTime _expiryDate;
        public DateTime ExpiryDate { get { return _expiryDate; } set { _expiryDate = value; OnPropertyChanged("ExpiryDate"); } }

        public int SecondsToExpiry { get { return (int)ExpiryDate.Subtract(DateTime.Now).TotalSeconds; } }

        public ExpiryViewModel()
        {
            this.ExpiryDate = DateTime.Today.AddDays(2.67);

            var timer = new System.Timers.Timer(1000);
            timer.Elapsed += (s, e) => OnPropertyChanged("SecondsToExpiry");
            timer.Start();
        }
    }
}

Ответ 2

вы можете комбинировать использование привязки с ресурсами, а также Свойства:

Пример:

XAML:

   <Window x:Class="Stackoverflow.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"  
        xmlns:local="clr-namespace:Stackoverflow"    
        Title="MainWindow" Height="350" Width="525">
         <Window.Resources>
           <local:StringFormatConverter x:Key="stringFormatConverter" />
           <sys:String x:Key="textResource">Kill me</sys:String>
         </Window.Resources>

         <Grid>
             <TextBlock>
                 <TextBlock.Text>
                     <MultiBinding Converter="{StaticResource stringFormatConverter}">
                          <Binding Path="SomeText" />
                          <Binding Source="{StaticResource textResource}" />                   
                      </MultiBinding>
                 </TextBlock.Text>
              </TextBlock>
          </Grid>
   </Window>

CS:

     public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this;
    }

    public string SomeText
    {
        get { return "Please"; }
    }

}

public class StringFormatConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return string.Format("{0} {1}", (string)values[0], (string)values[1]);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Изменить:

здесь работает сейчас

   <Window.Resources>
       <local:StringFormatConverter x:Key="stringFormatConverter" />
       <sys:String x:Key="textResource">Kill me</sys:String>
   </Window.Resources>

     <Grid>
         <TextBlock Tag="{DynamicResource textResource}">
             <TextBlock.Text>
                 <MultiBinding Converter="{StaticResource stringFormatConverter}">
                      <Binding Path="SomeText" />
                      <Binding Path="Tag" RelativeSource="{RelativeSource Self}" />                   
                  </MultiBinding>
             </TextBlock.Text>
          </TextBlock>
      </Grid>

Я подумаю о чем-то другом позже.

Ответ 3

Я знаю, что я точно не отвечаю на ваш вопрос, но в wpf уже есть механизм, который позволяет форматировать строки в xaml, это BindingBase.StringFormat собственность

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

     <TextBlock> 
        <TextBlock.Resources>
            <clr:String x:Key="ARG2ID">111</clr:String>
        </TextBlock.Resources>
    <TextBlock.Text> 
        <MultiBinding StringFormat="Name:{0}, Surname:{1} Age:{2}"> 
            <Binding Path="Name" />
            <Binding ElementName="txbSomeTextBox" Path="Text" Mode="OneWay" />
            <Binding Source="{StaticResource ARG2ID}" Mode="OneWay" />
        </MultiBinding> 
    </TextBlock.Text>
    </TextBlock>

Если вы действительно хотите реализовать собственное расширение разметки, которое берет привязку, есть способ. Я реализовал расширение разметки, которое принимает имя изображения (или привязки к чему-то, что его удерживает) как аргумент конструктора, затем разрешает путь и возвращает ImageSource.

Я реализовал его на основе этого artcle.

Поскольку я плохо разбираюсь, я лучше проиллюстрирую его с помощью кода:

<Image  Name="imgPicture"
             Source="{utils:ImgSource {Binding Path=DataHolder.PictureName}}" />
<Image  Name="imgPicture"
             Source="{utils:ImgSource C:\\SomeFolder\\picture1.png}" />
<Image  Name="imgPicture"
             Source="{utils:ImgSource SomePictureName_01}" />

класс расширения:

    public class ImgSourceExtension : MarkupExtension
        {
            [ConstructorArgument("Path")] // IMPORTANT!!
            public object Path { get; set; }

            public ImgSourceExtension():base() { }

            public ImgSourceExtension(object Path)
                : base()
            {
                this.Path = Path;
            }

            public override object ProvideValue(IServiceProvider serviceProvider)
            {
                object returnValue = null;
                try
                {
                    IProvideValueTarget service = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));

                    Binding binding = null;

                    if (this.Path is string)
                    {
                        binding = new Binding { Mode = BindingMode.OneWay };
                    }
                    else if (this.Path is Binding)
                    {
                        binding = Path as Binding;
                    }
else  if (this.Path is ImageSource) return this.Path;
                else if (this.Path is System.Windows.Expression)
                {
                    ResourceReferenceExpressionConverter cnv = new ResourceReferenceExpressionConverter();
                    DynamicResourceExtension mex = null;
                    try
                    {
                        mex = (MarkupExtension)cnv.ConvertTo(this.Path, typeof(MarkupExtension))
                            as DynamicResourceExtension;
                    }
                    catch (Exception) { }

                    if (mex != null)
                    {
                        FrameworkElement targetObject = service.TargetObject as FrameworkElement;
                        if (targetObject == null)
                        {
                            return Utils.GetEmpty(); 
                        }
                        return targetObject.TryFindResource(mex.ResourceKey as string);
                    }
                }
                    else return Utils.GetEmpty();


                    binding.Converter = new Converter_StringToImageSource();
                    binding.ConverterParameter = Path is Binding ? null : Path as string;

                    returnValue = binding.ProvideValue(serviceProvider);
                }
                catch (Exception) { returnValue = Utils.GetEmpty(); }
                return returnValue;
            }
        }

Конвертер:

[ValueConversion(typeof(string), typeof(ImageSource))]
    class Converter_StringToImageSource : MarkupExtension, IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            try
            {
                var key = (value as string ?? parameter as string);

                if (!string.IsNullOrEmpty(key))
                {
                    // Do translation based on the key
                    if (File.Exists(key))
                    {
                        var source = new BitmapImage(new Uri(key));
                        return source;
                    }
                    else
                    {
                        var source = new BitmapImage(new Uri(Utils.GetPicturePath(key)));
                        return source;
                    }

                }
                return Utils.GetEmpty();
            }
            catch (Exception)
            {
                return Utils.GetEmpty();
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        public Converter_StringToImageSource()
            : base()
        {
        }

        private static Converter_StringToImageSource _converter = null;

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (_converter == null) _converter = new Converter_StringToImageSource();
            return _converter;
        }
    }

EDIT:

Я обновил ImgSourceExtension, так что теперь он будет работать со StaticResource и DynamicResource, хотя я до сих пор не знаю, как сделать вид вложенной привязки, которую ищет OP.

Сказав это, во время моего вчерашнего исследования я наткнулся на интересный "взлом" , связанный с привязкой к динамическим ресурсам. Я думаю, что объединение его с SortedList или другим типом данных коллекции, к которым можно получить доступ по ключу, может стоить посмотреть:

 xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
 xmlns:sys="clr-namespace:System;assembly=mscorlib"
...
<Window.Resources>
        <col:SortedList x:Key="stringlist">
            <sys:String x:Key="key0">AAA</sys:String>
            <sys:String x:Key="key1">BBB</sys:String>
            <sys:String x:Key="key2">111</sys:String>
            <sys:String x:Key="key3">some text</sys:String>
        </col:SortedList>
    </Window.Resources>
....
   <TextBlock Name="txbTmp" DataContext="{DynamicResource stringlist}"> 
        <TextBlock.Text> 
            <MultiBinding StringFormat="Name:{0}, Surname:{1} Age:{2}"> 
                <Binding Path="[key0]" />
                <Binding Path="[key1]"/>
                <Binding Path="[key2]" />
            </MultiBinding> 
        </TextBlock.Text>
    </TextBlock>

Единственный недостаток, с которым я столкнулся, заключается в том, что при изменении значений в stringlist ресурс должен быть переназначен:

  SortedList newresource = new SortedList(((SortedList)Resources["stringlist"]));
  newresource["key0"] = "1234";
  this.Resources["stringlist"] = newresource;

Ответ 4

Я думаю, что я просто решил старую проблему переключения культуры во время выполнения довольно аккуратно.

enter image description hereenter image description hereenter image description here

Как я вижу это, есть две возможности:

  • Мы согласны с тем, что вам понадобится DynamicResources для вашей локализации и напишите расширение разметки, что в значительной степени связано с тем, что вы пробовали и чего трудно достичь.
  • Мы просто используем StaticResources, и в этом случае мир привязок становится намного проще, но обновление уже связанных строк становится более сложным.

Я предлагаю последнее. В принципе, моя идея - использовать прокси-сервер для файла resx, который может обновлять все привязки после изменения культуры. Эта статья статьи OlliFromTor прошла долгий путь к обеспечению реализации.

Для более глубокого вложения существует ограничение на то, что StringFormat не принимает привязки, поэтому вам все равно придется вводить конвертер, если StringFormats не может оставаться статическим.

Структура Resx:

enter image description here

Содержимое Resx (по умолчанию /no/es ):

enter image description here

enter image description here

enter image description here

Xaml:

<UserControl x:Class="WpfApplication1.Controls.LoginView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:props="clr-namespace:WpfApplication1.Properties"
             xmlns:models="clr-namespace:WpfApplication1.Models"
             Background="#FCF197" 
             FontFamily="Segoe UI"
             TextOptions.TextFormattingMode="Display"> <!-- please notice the effect of this on font fuzzyness -->

    <UserControl.DataContext>
        <models:LoginViewModel />
    </UserControl.DataContext>
    <UserControl.Resources>
        <Thickness x:Key="StdMargin">5,2</Thickness>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
        </Style>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
            <Setter Property="MinWidth" Value="80"/>
        </Style>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
        </Style>
        <Style TargetType="{x:Type ComboBox}">
            <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
        </Style>

    </UserControl.Resources>

    <Grid Margin="30" Height="150" Width="200">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*" MinWidth="120"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Username, Source={StaticResource Resx}}" />
        <TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Password, Source={StaticResource Resx}}" />
        <TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding Language, Source={StaticResource Resx}}" />
        <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbxUsername" Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" />
        <TextBox Grid.Row="1" Grid.Column="1" x:Name="tbxPassword" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}" />
        <ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Cultures}" DisplayMemberPath="DisplayName" SelectedItem="{Binding SelectedCulture}" />
        <TextBlock Grid.Row="3" Grid.ColumnSpan="2" Foreground="Blue" TextWrapping="Wrap" Margin="5,15,5,2">
            <TextBlock.Text>
                <MultiBinding StringFormat="{x:Static props:Resources.LoginMessage}">
                    <Binding Path="Username" />
                    <Binding Path="Password" />
                    <Binding Path="Language" Source="{StaticResource Resx}" />
                    <Binding Path="SelectedCulture.DisplayName" FallbackValue="(not set)" />
                </MultiBinding>
            </TextBlock.Text>
        </TextBlock>
    </Grid>
</UserControl>

Я решил добавить экземпляр ResourceProxy к App.xaml, есть другие возможности (например, создание экземпляров и просмотр прокси непосредственно на ViewModel)

<Application x:Class="WpfApplication1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:props="clr-namespace:WpfApplication1.Properties"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <props:ResourcesProxy x:Key="Resx" />
    </Application.Resources>
</Application>

ViewModel:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Threading;
using System.Windows;
using WpfApplication1.Properties;

namespace WpfApplication1.Models
{
    public class LoginViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

            if (propertyName == "SelectedCulture")
                ChangeCulture();
        }

        private ObservableCollection<CultureInfo> _cultures;
        public ObservableCollection<CultureInfo> Cultures { get { return _cultures; } set { _cultures = value; OnPropertyChanged("Cultures"); } }

        private CultureInfo _selectedCulture;
        public CultureInfo SelectedCulture { get { return _selectedCulture; } set { _selectedCulture = value; OnPropertyChanged("SelectedCulture"); } }

        private string _username;
        public string Username { get { return _username; } set { _username = value; OnPropertyChanged("Username"); } }

        private string _password;
        public string Password { get { return _password; } set { _password = value; OnPropertyChanged("Password"); } }

        public LoginViewModel()
        {
            this.Cultures = new ObservableCollection<CultureInfo>()
            {
                new CultureInfo("no"),
                new CultureInfo("en"),
                new CultureInfo("es")
            };
        }

        private void ChangeCulture()
        {
            Thread.CurrentThread.CurrentCulture = this.SelectedCulture;
            Thread.CurrentThread.CurrentUICulture = this.SelectedCulture;

            var resx = Application.Current.Resources["Resx"] as ResourcesProxy;
            resx.ChangeCulture(this.SelectedCulture);
        }
    }
}

И, наконец, важная часть, ResourceProxy:

using System.ComponentModel;
using System.Dynamic;
using System.Globalization;
using System.Linq;
using System.Reflection;

namespace WpfApplication1.Properties
{
    /// <summary>
    /// Proxy to envelop a resx class and attach INotifyPropertyChanged behavior to it.
    /// Enables runtime change of language through the ChangeCulture method.
    /// </summary>
    public class ResourcesProxy : DynamicObject, INotifyPropertyChanged
    {
        private Resources _proxiedResources = new Resources(); // proxied resx

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(_proxiedResources, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Sets the new culture on the resources and updates the UI
        /// </summary>
        public void ChangeCulture(CultureInfo newCulture)
        {
            Resources.Culture = newCulture;

            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(null));
        }

        private PropertyInfo GetPropertyInfo(string propertyName)
        {
            return _proxiedResources.GetType().GetProperties().First(pi => pi.Name == propertyName);
        }

        private void SetMember(string propertyName, object value)
        {
            GetPropertyInfo(propertyName).SetValue(_proxiedResources, value, null);
            OnPropertyChanged(propertyName);
        }

        private object GetMember(string propertyName)
        {
            return GetPropertyInfo(propertyName).GetValue(_proxiedResources, null);
        }

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            if (binder.Type == typeof(INotifyPropertyChanged))
            {
                result = this;
                return true;
            }

            if (_proxiedResources != null && binder.Type.IsAssignableFrom(_proxiedResources.GetType()))
            {
                result = _proxiedResources;
                return true;
            }
            else
                return base.TryConvert(binder, out result);
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            result = GetMember(binder.Name);
            return true;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            SetMember(binder.Name, value);
            return true;
        }
    }
}