带有标记扩展名的字符串格式

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

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

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

           

有明显的StringFormatConveter实现。

我试图实现自定义标记扩展,以便语法如下:

          

或者只是

  

但是我坚持执行ProvideValue(IServiceProvider serviceProvider) ,因为参数是另一个标记扩展。

互联网上的大多数例子都是微不足道的:它们要么根本不使用serviceProvider ,要么查询IProvideValueTarget ,它们(大多数)说的是什么依赖属性是标记扩展的目标。 在任何情况下,代码都知道在ProvideValue调用时应该提供的值。 但是, ProvideValue只会被调用一次( 模板除外 ,这是一个单独的故事),因此如果实际值不是常数(如Binding等),则应该使用另一种策略。

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

  1. BindingExpression / ResourceReferenceExpression类型的对象如何不按“原样”处理,但是被要求提供基础值?
  2. MultiBindingExpression如何知道底层绑定的值已经改变,所以它也必须使其值无效?

我实际上发现了一个标记扩展库实现,声称支持连接字符串(完全映射到我的用例)( 项目 , 代码 ,依赖于其他代码的连接实现 ),但它似乎只支持嵌套扩展库类型(即,你不能在里面嵌套一个vanilla Binding )。

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


实际上我使用自定义隐形辅助UI元素实现了所需的语义

      

(其中FormatHelper跟踪其子项及其依赖项属性更新,并将最新结果存储到Value ),但这种语法似乎很难看,我想摆脱可视树中的帮助项。


最终的目标是促进翻译:像“15秒直到爆炸”之类的UI字符串自然地表示为可本地化的格式“{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] )(将多绑定溢出添加到外部绑定中)来解决这个问题,这可能适用于嵌套的多绑定和格式扩展,但是仍然会因嵌套而失败动态资源。

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


更新报告 :遗憾的是,答案中提出的想法并未解决问题。 也许它certificate了标记扩展虽然是function强大且function多样的工具,但需要WPF团队更多关注。

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


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

但是,@ adabyron有一个改进的想法,这有助于隐藏主机项中的辅助元素(但是这样做的价格是主机的子类)。 我将尝试看看是否有可能摆脱子类化(使用劫持主机的LogicalChildren的行为并添加帮助元素到我的脑海中,受到相同答案的旧版本的启发)。

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

在此处输入图像描述在此处输入图像描述

编辑 :我已经用TextBlock的子类替换了混合行为。 这为DataContext和DynamicResources添加了更容易的链接。

在旁注中,您的项目使用DynamicResources引入条件的方式不是我建议的。 而是尝试使用ViewModel来建立条件,和/或使用触发器。

XAML:

       {0:d}                 

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); }; } ///  /// Reacts to a new topmost StringFormatContainer ///  private void OnContentChanged(object sender, EventArgs e) { this.AddLogicalChild(this.Content); // inherits DataContext _valueChangedHandler(this, EventArgs.Empty); } ///  /// Updates Text to the Content values ///  private void UpdateText() { this.Text = Content.GetValue() as string; } ///  /// Attaches listeners for changes in the Content tree ///  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 } ///  /// Detaches listeners ///  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), typeof(StringFormatContainer), new FrameworkPropertyMetadata(new ObservableCollection())); public static readonly DependencyProperty ValuesProperty = ValuesPropertyKey.DependencyProperty; public ObservableCollection Values { get { return (ObservableCollection)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()); this.Values.CollectionChanged += OnValuesChanged; } ///  /// The implementation of LogicalChildren allows for DataContext propagation. /// This way, the DataContext needs only be set on the outermost instance of StringFormatContainer. ///  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); } } ///  /// Recursive function to piece together the value from the StringFormatContainer hierarchy ///  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(); } } } 

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

样品:

XAML:

     Kill me             

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

编辑:

这是现在的工作

    Kill me            

我以后会想到别的东西。

我知道我并没有完全回答你的问题,但是在wpf中已经存在一种允许在xaml中进行字符串格式化的机制,它是BindingBase.StringFormat属性

我还没有弄清楚如何使它与DynamicResource绑定一起工作,但它适用于其他绑定,例如绑定到数据上下文的属性,静态资源或另一个元素的属性。

    111          

如果你真的想要实现自己的带有绑定的标记扩展,那么有一种方法。 我实现了一个标记扩展,它将图片的名称(或绑定到保存它的东西)作为构造函数参数,然后解析路径并返回ImageSource。

我是基于这个artcle实现的 。

由于我不善于解释,我最好使用代码来说明它:

    

扩展类:

  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正在寻找的嵌套绑定。

话虽如此,在我昨天的研究中,我偶然发现了一个与动态资源绑定相关的有趣“黑客” 。 我认为将它与SortedList或其他可以通过key访问的集合数据类型相结合可能值得研究:

  xmlns:col="clr-namespace:System.Collections;assembly=mscorlib" xmlns:sys="clr-namespace:System;assembly=mscorlib" ...   AAA BBB 111 some text   ....          

我遇到的唯一缺点是,在更改stringlist的值时,必须重新分配资源:

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

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

在此处输入图像描述在此处输入图像描述在此处输入图像描述

我看待它的方式有两种可能性:

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

我建议后者。 基本上我的想法是使用resx文件的代理,它可以在文化发生变化时更新所有绑定。 OlliFromTor的这篇文章在提供实现方面走了很长的路。

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

Resx结构:

在此处输入图像描述

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

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

XAML:

       5,2                                   

我选择将ResourcesProxy的实例添加到App.xaml,还有其他可能性(例如直接在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 _cultures; public ObservableCollection 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() { 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 { ///  /// Proxy to envelop a resx class and attach INotifyPropertyChanged behavior to it. /// Enables runtime change of language through the ChangeCulture method. ///  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)); } ///  /// Sets the new culture on the resources and updates the UI ///  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; } } }