【问题标题】:String format with a markup extension带有标记扩展的字符串格式
【发布时间】:2014-10-14 10:35:48
【问题描述】:

我试图让string.Format 在 WPF 中作为一个方便的函数使用,以便可以在纯 XAML 中组合各种文本部分,而无需代码隐藏中的样板。主要问题是支持函数的参数来自其他嵌套标记扩展(例如Binding)的情况。

实际上,有一个功能非常接近我的需要:MultiBinding。不幸的是,它只能接受绑定,不能接受其他动态类型的内容,例如DynamicResources。

如果我所有的数据源都是绑定,我可以像这样使用标记:

<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 只会被调用一次(except for templates,这是一个单独的故事),因此如果实际值不是恒定的(例如 Binding 等),则应使用另一种策略。

我在 Reflector 中查找了Binding 的实现,它的ProvideValue 方法实际上返回的不是真正的目标对象,而是System.Windows.Data.BindingExpression 类的实例,这似乎完成了所有实际工作。 DynamicResource 也是如此:它只返回一个 System.Windows.ResourceReferenceExpression 的实例,它关心订阅(内部)InheritanceContextChanged 并在适当的时候使值无效。但是,通过查看代码我无法理解的是:

  1. BindingExpression / ResourceReferenceExpression 类型的对象没有按“原样”处理,而是要求提供基础值,这是怎么发生的?
  2. MultiBindingExpression 如何知道底层绑定的值已更改,因此它也必须使其值无效?

我实际上找到了一个标记扩展库实现,它声称支持连接字符串(这完全映射到我的用例)(projectcodeconcatenation implementation 依赖于other code),但是它似乎只支持库类型的嵌套扩展(即,你不能在里面嵌套一个 vanilla Binding)。

有没有办法实现问题顶部的语法?它是受支持的场景,还是只能从 WPF 框架内部执行此操作(因为 System.Windows.Expression 具有内部构造函数)?


实际上,我使用自定义的不可见帮助器 UI 元素实现了所需的语义

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

这似乎适用于嵌套绑定和动态资源,但在尝试将其嵌套在自身中时却惨遭失败,因为在这种情况下,从IProvideValueTarget 获得的targetObjnull。我试图通过将嵌套绑定合并到外部绑定([1a][2a])来解决这个问题(将多重绑定溢出添加到外部绑定中),这可能适用于嵌套的多重绑定和格式扩展,但仍然会因嵌套而失败动态资源。

有趣的是,当嵌套不同类型的标记扩展时,我在外部扩展中得到Bindings 和MultiBindings,但是ResourceReferenceExpression 而不是DynamicResourceExtension。我想知道为什么不一致(以及Binding 是如何从BindingExpression 重构的)。


更新报告:不幸的是,答案中给出的想法并没有带来问题的解决方案。或许它证明了标记扩展,虽然是相当强大和通用的工具,但需要 WPF 团队更多的关注。

无论如何,我感谢所有参与讨论的人。提出的部分解决方案足够复杂,值得更多支持。


更新报告:似乎没有很好的标记扩展解决方案,或者至少创建一个所需的 WPF 知识水平太深而无法实用。

然而,@adabyron 有一个改进的想法,这有助于隐藏宿主项目中的辅助元素(然而,这样做的代价是对宿主进行子类化)。我会尝试看看是否有可能摆脱子类化(使用劫持主机的 LogicalChildren 并为其添加辅助元素的行为出现在我的脑海中,灵感来自同一答案的旧版本)。

【问题讨论】:

  • 每次我创建自定义标记扩展时,我最终都会删除它并找到一种更简单的方法来完成需要完成的工作、转换器、附加属性,还有很多简单的使用技术..

标签: c# wpf markup-extensions


【解决方案1】:

您可以将 Binding 与 Resources 以及 Properties 结合使用:

示例:

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>

我稍后再想点别的。

【讨论】:

  • 不确定这是否也适用于DynamicResource。 (我一到编译器就会检查它。)对于本地化,DynamicResource 是主要方案。
  • 不幸的是,它不起作用。将 StaticResource 替换为 DynamicResource 会产生运行时异常:A 'DynamicResourceExtension' cannot be set on the 'Source' property of type 'Binding'. A 'DynamicResourceExtension' can only be set on a DependencyProperty of a DependencyObject.
  • 关于解决方法:我想过,但是 (1) Tag 已经可以用于不同的东西,(2) 每个 UI 对象只允许一个 DynamicResource。这当然可以通过声明“足够”数量的附加属性来解决,但该解决方案并不比问题中的最后一个代码 sn-p 更好。 (诀窍是定义足够多的 DP 并将它们用作DynamicResourceBinding 之间的桥梁。)
  • 我想的是一个 DP,它的类型是资源的集合,我需要研究如何做到这一点。
【解决方案2】:

我知道我没有完全回答你的问题,但是 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。

我是基于this 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;
        }
    }

编辑:

我更新了 ImgSourceExtension,所以现在它可以与 StaticResource 和 DynamicResource 一起使用,尽管我仍然不知道如何执行 OP 正在寻找的那种嵌套绑定。

话虽如此,在我昨天的研究中,我偶然发现了与绑定到动态资源相关的an interesting "hack"。我认为将它与 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;

【讨论】:

  • 好吧,StringFormat 的问题不是依赖属性(因为MultiBinding 本身不是DependencyObject),而MultiBinding 没有做任何技巧来接受依赖属性那里。此外,我认为以这种方式使用DynamicResources 是不可能的(除了声明额外的依赖属性,它已经在@eran 的答案的cmets 中讨论过)。
  • @Vlad 昨天我找到了一种使用动态资源作为控制数据源的方法,然后进行多重绑定。我更新了我的答案。
  • ImgSourceExtension的更新版本处理DynamicResource,但它会立即解析资源(targetObject.TryFindResource),所以这意味着如果资源值稍后更改,新值不会被捡起来。 (这对于本地化至关重要,这是我的主要目标。)关于DataContext 的技巧,它与@eran 提出的Tag 的技巧基本相同,并且具有相同的问题:如果DataContext 用于不同的东西,或者如果我们在表达式中需要多个 DynamicResource
【解决方案3】:

看看以下是否适合你。我接受了您在评论中提供的test case 并稍微扩展了它以更好地说明机制。我想关键是通过在嵌套容器中使用DependencyProperties 来保持灵活性。

编辑:我已将混合行为替换为 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>

文本块复合体:

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

字符串格式容器:

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

【讨论】:

  • 这似乎更好,谢谢!让我试试。我想知道这些行为是否会进入可视化树,以便它们可以更新动态资源绑定。
  • 作为对您的回答不是,实际上该项目不会将动态资源用于业务逻辑/条件。我们使用 e。 G。应用程序范围内的 UserPreferredDateFormat 而不是 ShortOrLongDateFormat,因此我们不需要将其传播到所有单个 VM。
  • 好吧,我已经尝试过您的解决方案。它适用于您的示例,我真的很喜欢这种技术(扩展逻辑树特别好!)。不幸的是,它没有接收到DynamicResources 的变化。这是因为行为不是外部逻辑树的一部分,因此不会得到外部动态资源的更新。这也是不继承DataContext 的原因,也许也是不激活内部绑定的原因。无论如何,感谢您对这个问题的关注!
  • 我想这是一个可以克服的问题。什么情况,你是交换整个字典,还是按键更改单个资源?对于后者,解决方法很简单——您只需要确保相关资源存在于TextBindingBehavior.Content 资源中。例如。在 OnAttached:this.Content.Resources.MergedDictionaries.Add(Application.Current.Resources);
  • 好吧。在典型的本地化场景中,更新的是Application.Current 资源的合并字典(在某个未知的嵌套深度上)。但是对于像UserPreferredDateFormat 这样的场景,可以通过外部代码在任何位置(不一定在Application/Window 级别)设置(和覆盖)该值,所以我不能依赖它在某个指定位置.
【解决方案4】:

我想我刚刚很好地解决了运行时切换文化的老问题。

在我看来,有两种可能:

  1. 我们接受您需要 DynamicResources 进行本地化并编写标记扩展,这几乎是您尝试过的,但似乎很难实现。
  2. 我们只使用 StaticResources,在这种情况下,绑定世界变得更加容易,但更新已经绑定的字符串变得更加棘手。

我建议后者。基本上我的想法是使用 resx 文件的代理,一旦文化发生变化,它就能够更新所有绑定。这个article by OlliFromTor 在提供实现方面大有帮助。

对于更深层次的嵌套,存在 StringFormat 不接受绑定的限制,因此如果 StringFormat 不能保持静态,您可能仍需要引入转换器。

Resx 结构:

Resx 内容(默认/否/es):

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>

我选择将 ResourcesProxy 的实例添加到 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>

视图模型:

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

最后是重要的部分,ResourcesProxy:

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

【讨论】:

  • 嗯,我在这里看到了两点。首先,将DynamicResources 包含到标记扩展的数据源中是我最终想要实现的目标。我知道其他解决方案,但我需要此功能的应用程序选择了基于ResourceDicitonary 的本地化。除此之外,DynamicResources 可以来自其他来源,而不是本地化需求,它们也需要得到支持。正如您在回答中提出的那样,使用 MultiBinding 在需要嵌套时无济于事:它也不接受其他 MultiBinding 作为数据源。
  • 第二,我看不到DynamicObject的必要性:如果我在编译时就知道要代理哪个类型,我可以自己定义属性而不需要全部DynamicObject 的功率(和性能损失)。
  • 首先,正如我所说,为了更深层次的嵌套,转换器可能是必要的。您能否提供一个应该解决的(嵌套)示例?您的最终目标是关于翻译,除了可以轻松重定位到 resx 文件中的字符串之外,您还会使用哪些其他动态资源?其次,DynamicObject 提供了对基类成员进行自动代理所需的确切机制,其中包含可以引发 PropertyChanged 的​​ TryGet/SetMember 方法。
  • 好吧,我没有找到使用转换器进行嵌套的方法。最终目标是翻译,但DynamicResource 可以用于其他(与翻译无关)的事情。您可以通过在外部ResourceDictionary 中设置资源来为UserControl 提供任何值,因此资源值不取决于语言,而是取决于其他程序逻辑。
  • This idea(添加一个监听语言变化并报告属性变化的中间对象)似乎更简单,不需要 DLR。无论如何,我正在从事的项目中的翻译不是通过 resx 完成的。
猜你喜欢
  • 1970-01-01
  • 2012-09-01
  • 2012-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-20
相关资源
最近更新 更多