12

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

实际上,有一个非常接近我需要的功能:MultiBinding. 不幸的是,它只能接受bindings,但不能接受其他动态类型的内容,例如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,要么 query IProvideValueTarget(大部分)说明了标记扩展的目标是什么依赖属性。在任何情况下,代码都知道应该在ProvideValue调用时提供的值。但是,ProvideValue只会被调用一次(模板除外Binding,这是一个单独的故事),因此如果实际值不是恒定的(例如 for等),则应使用另一种策略。

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

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

我实际上找到了一个标记扩展库实现,它声称支持连接字符串(这完全映射到我的用例)(项目代码、依赖其他代码的连接实现),但它似乎只支持嵌套扩展库类型(即,您不能在其中嵌套香草)。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),但是这种语法似乎很难看,我想摆脱可视树中的帮助项。


最终目标是促进翻译:像“15 seconds until explode”这样的 UI 字符串自然地表示为可本地化的格式“{0} until explode”(进入 aResourceDictionary并且将在语言更改时被替换)和BindingVM 依赖项表示时间的属性。


更新报告:我尝试使用我在互联网上找到的所有信息自己实现标记扩展。完整的实现在这里([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);

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

有趣的是,当嵌套不同类型的标记扩展时,我在外部扩展中得到Bindings 和s,而不是. 我想知道为什么它不一致(以及如何从重建)。MultiBindingResourceReferenceExpressionDynamicResourceExtensionBindingBindingExpression


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

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


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

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

4

4 回答 4

3

您可以将 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>

我稍后再想点别的。

于 2014-08-20T23:35:00.150 回答
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。

我根据这篇文章实现了它。

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

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

话虽如此,在我昨天的研究中,我偶然发现了一个与绑定到动态资源相关的有趣“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;
于 2014-08-28T12:22:09.900 回答
1

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

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

在我看来,有两种可能:

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

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

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

Resx结构:

在此处输入图像描述

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

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

xml:

<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;
        }
    }
}
于 2014-08-31T15:54:12.947 回答
1

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

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

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

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

xml:

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

过期视图模型:

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();
        }
    }
}
于 2014-09-06T12:14:54.620 回答