与标记扩展字符串格式 [英] String format with a markup extension

查看:139
本文介绍了与标记扩展字符串格式的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图让的String.Format 可作为WPF一个方便的功能,使各种文本部分可以在纯XAML组合,无需代码 - 样板背后。主要的问题是支持的,其中函数的自变量是从其他嵌套标记扩展来(如绑定)。

I am trying to make string.Format available as a handy function in WPF, so that the various text parts can be combined in pure XAML, without boilerplate in code-behind. The main problem is support of the cases where the arguments to the function are coming from other, nested markup extensions (such as Binding).

其实,有一种特性,它是非常接近我所需要的: MultiBinding 。遗憾的是它只能接受绑定的,而不是其它动态类型的内容,如 DynamicResource 秒。

Actually, there is a feature which is quite close to what I need: MultiBinding. Unfortunately it can accept only bindings, but not other dynamic type of content, like DynamicResources.

如果我的所有数据来源是绑定的,我可以用类似这样的标记:

If all my data sources were bindings, I could use markup like this:

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

有明显的实施 StringFormatConveter

我想实现一个自定义标记扩展,使语法是这样的:

I tried to implement a custom markup extension so that the syntax is like that:

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



或者只是

or maybe just

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



不过,我停留在 ProvideValue(的IServiceProvider的ServiceProvider)的实施的说法是另一个标记扩展的情况下

But I am stuck at the implementation of ProvideValue(IServiceProvider serviceProvider) for the case of argument being another markup extension.

大多数在互联网的例子都是很琐碎的:他们要么不使用的ServiceProvider 所有,或查询 IProvideValueTarget ,其中(大部分)说,依赖项属性是什么标记扩展的目标。在任何情况下,代码知道应该在 ProvideValue 通话时提供的价值。然而, ProvideValue 将被调用一次(的除了,这是一个独立的故事模板),所以应该如果实际值不是恒定的使用另一种策略(如它是绑定等)。

Most of the examples in the internet are pretty trivial: they either don't use serviceProvider at all, or query IProvideValueTarget, which (mostly) says what dependency property is the target of the markup extension. In any case, the code knows the value which should be provided at the time of ProvideValue call. However, ProvideValue will be called only once (except for templates, which are a separate story), so another strategy should be used if the actual value is not constant (like it's for Binding etc.).

我抬头<$ C $执行C>绑定在反射器,其 ProvideValue 方法实际上返回而不是真正的目标对象,但 System.Windows.Data.BindingExpression 类的实例,这似乎做所有的实际工作。同样是关于 DynamicResource :它只是返回 System.Windows.ResourceReferenceExpression 的一个实例,这是关心订阅(内部) InheritanceContextChanged 并在适当的时候值无效。我却无法从通过代码看明白的是以下内容:

I looked up the implementation of Binding in Reflector, its ProvideValue method actually returns not the real target object, but an instance of System.Windows.Data.BindingExpression class, which seems to do all the real work. The same is about DynamicResource: it just returns an instance of System.Windows.ResourceReferenceExpression, which is caring about subscribing to (internal) InheritanceContextChanged and invalidating the value when appropriate. What I however couldn't understand from looking through the code is the following:


  1. 它是如何发生的那种类型的对象 BindingExpression / ResourceReferenceExpression 不被视为原样,但被要求为基础值?

  2. 如何 MultiBindingExpression 知道底层绑定的值发生了变化,所以它不得不作废它的价值呢?

  1. How does it happen that the object of type BindingExpression / ResourceReferenceExpression is not treated "as is", but is asked for the underlying value?
  2. How does MultiBindingExpression know that the values of the underlying bindings have changed, so it have to invalidate its value as well?

其实我已经发现了一个标记扩展库实现它声称支持并置字符串(这是完全映射到我的用例)(的项目代码,在级联实现依托的其他代码),但它似乎支持嵌套只扩展的库类型(比如,你不能嵌套香草绑定内)。

I have actually found a markup extension library implementation which claims to support concatenating the strings (which is perfectly mapping to my use case) (project, code, the concatenation implementation relying on other code), but it seems to support nested extensions only of the library types (i.e., you cannot nest a vanilla Binding inside).

有没有实现的语法方式在这个问题上提出的?它是一个支持的方案,或者一个只能从WPF框架内做到这一点(因为 System.Windows.Expression 有一个内部的构造函数)?

Is there a way to implement the syntax presented at the top of the question? Is it a supported scenario, or one can do this only from inside the WPF framework (because System.Windows.Expression has an internal constructor)?

其实我有所需的语义的实施的使用自定义的隐形帮手UI元素:

Actually I have an implementation of the needed semantics using a custom invisible helper UI element:

<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 跟踪其子女和其依赖属性更新,并存储了最新结果为),但这个语法似乎是丑,我想在可视化树摆脱帮手项目

(where FormatHelper tracks its children and its dependency properties update, and stores the up-to-date result into Value), but this syntax seems to be ugly, and I want to get rid of helper items in visual tree.

的最终目的是为了方便翻译:像UI字符串十五秒钟,直至爆炸自然表示为本地化格式为{0},直到爆炸(这将进入 ResourceDictionary中键,将被替换时,语言的变化)和绑定为代表的时间VM依赖属性。

The ultimate goal is to facilitate the translation: UI strings like "15 seconds till explosion" are naturally represented as localizable format "{0} till explosion" (which goes into a ResourceDictionary and will be replaced when the language changes) and Binding to the VM dependency property representing the time.

更新报表:我试图执行标记扩展自己所有我能找到在互联网上的信息。全面实施是在这里( [1] ,的 [2] [3] ),这里是最核心的部分:

Update report: I tried to implement the markup extension myself with all the information I could find in internet. Full implementation is here ([1], [2], [3]), here is the core part:

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 获得的 targetObj 。我试图解决这个与合并嵌套绑定到外一( [1A] ,的 [2A] )(加入multibinding溢出到外部绑定),这将或许与嵌套multibindings和格式的扩展工作,但剧照失败,嵌套动态资源。

This seems to work with nesting bindings and dynamic resources, but fails miserably on try to nest it in itself, as in this case targetObj obtained from IProvideValueTarget is null. I tried to work around this with merging the nested bindings into the outer one ([1a], [2a]) (added multibinding spill into outer binding), this would perhaps work with the nested multibindings and format extensions, but stills fails with nested dynamic resources.

有趣的是,嵌套不同类型的标记扩展的时候,我得到绑定和<$ C在外部扩展$ C> MultiBinding S,但 ResourceReferenceExpression 而不是 DynamicResourceExtension 。我不知道为什么会不一致(又是怎样的绑定 BindingExpression 重建)。

Interesting enough, when nesting different kinds of markup extensions, I get Bindings and MultiBindings in the outer extension, but ResourceReferenceExpression instead of DynamicResourceExtension. I wonder why is it inconsistent (and how is the Binding reconstructed from BindingExpression).

更新报表:可惜在给出答案的想法并没有带来问题的解决方案。也许这证明标记扩展,同时是相当强大和灵活的工具,需要从WPF团队更多的关注。

Update report: unfortunately the ideas given in answers didn't bring the solution of the problem. Perhaps it proves that the markup extensions, while being quite powerful and versatile tool, need more attention from WPF team.

反正我要感谢的人谁在参与讨论。 。这是提出的部分解决方案足够复杂,应该得到更多upvotes

Anyway I thank to anyone who took part in the discussion. The partial solutions which were presented are complicated enough to deserve more upvotes.

更新报表:似乎有与标记扩展没有好的解决办法,或至少需要制作一个WPF知识水平太深而不实用。

Update report: there seems to be no good solution with markup extensions, or at least the level of WPF knowledge needed for creating one is too deep to be practical.

然而,@adabyron过改进的想法,这有助于隐藏在宿主项辅助元素(这价格却继承主机)。我会尝试看看是否有可能摆脱子类(使用行为,劫持主机的LogicalChildren,并添加辅助元素,它涉及到我的脑海里,由旧版本相同的答案的启发)。

However, @adabyron had an idea of improvement, which helps to hide the helper elements in the host item (the price of this is however subclassing the host). I'll try to see if it's possible to get rid of subclassing (using a behaviour which hijacks the host's LogicalChildren and adds helper elements to it comes to my mind, inspired by the old version of the same answer).

推荐答案

看看以下为你工作。我接过测试用例你在注释提供和扩大稍微更好地说明的机制。我想关键是在嵌套容器中使用 DependencyProperties ,以保持灵活性。

See if the following works for you. I took the test case you offered in the comment and expanded it slightly to better illustrate the mechanism. I guess the key is to keep flexibility by using DependencyProperties in the nesting container.

修改:我已经取代了混合行为与TextBlock的一个子类。这增加了对的DataContext和DynamicResources容易联动。

EDIT: I have replaced the blend behavior with a subclass of the TextBlock. This adds easier linkage for DataContext and DynamicResources.

在阿里纳斯,您的项目使用 DynamicResources 来介绍情况的方式是不是我会推荐。而是尝试使用视图模型建立的条件,和/或使用触发器

On a sidenote, the way your project uses DynamicResources to introduce conditions is not something I would recommend. Instead try using the ViewModel to establish the conditions, and/or use Triggers.

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:

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:

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

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

这篇关于与标记扩展字符串格式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆