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

时间:2014-08-20 22:19:12

标签: c# wpf markup-extensions

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

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

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

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

明显实施StringFormatConveter

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

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

或者只是

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

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

互联网上的大多数示例都非常简单:他们要么根本不使用serviceProvider,要么查询IProvideValueTarget,其中(大多数)说的是什么依赖属性是目标标记扩展名。在任何情况下,代码都知道在ProvideValue调用时应该提供的值。但是,ProvideValue只会被调用一次(except for templates,这是一个单独的故事),所以如果实际值不是常数,则应该使用另一种策略(就像它{{1}一样等等。)。

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

  1. 如何处理InheritanceContextChanged / BindingExpression类型的对象未被处理&#34;按原样#34;但是被要求提供基础值?
  2. ResourceReferenceExpression如何知道底层绑定的值已经改变,所以它也必须使其值无效?
  3. 我实际上发现了一个标记扩展库实现,声称支持连接字符串(完全映射到我的用例)(projectcodeconcatenation implementation依赖于other code),但它似乎只支持库类型的嵌套扩展(即,你不能在其中嵌套一个vanilla MultiBindingExpression。)

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


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

    System.Windows.Expression

    (其中<l:FormatHelper x:Name="h1" Format="{DynamicResource FORMAT_ID'"> <l:FormatArgument Value="{Binding Data1}"/> <l:FormatArgument Value="{StaticResource Data2}"/> </l:FormatHelper> <TextBlock Text="{Binding Value, ElementName=h1}"/> 跟踪其子项及其依赖项属性更新,并将最新结果存储到FormatHelper),但这种语法似乎很难看,我想要摆脱视觉树中的辅助项目。


    最终目标是促进翻译:UI字符串如“爆炸前15秒”#34;自然地表示为可本地化的格式&#34; {0}直到爆炸&#34; (进入Value并在语言更改时替换)和ResourceDictionary到表示时间的VM依赖属性。


    更新报告:我尝试使用我在互联网上找到的所有信息来实现标记扩展。完整实施在这里([1][2][3]),这是核心部分:

    Binding

    这似乎与嵌套绑定和动态资源一起使用,但在尝试将其嵌套在自身时失败,因为在这种情况下var result = new MultiBinding() { Converter = new StringFormatConverter(), Mode = BindingMode.OneWay }; foreach (var v in values) { if (v is MarkupExtension) { var b = v as Binding; if (b != null) { result.Bindings.Add(b); continue; } var bb = v as BindingBase; if (bb != null) { targetObjFE.SetBinding(AddBindingTo(targetObjFE, result), bb); continue; } } if (v is System.Windows.Expression) { DynamicResourceExtension mex = null; // didn't find other way to check for dynamic resource try { // rrc is a new ResourceReferenceExpressionConverter(); mex = (MarkupExtension)rrc.ConvertTo(v, typeof(MarkupExtension)) as DynamicResourceExtension; } catch (Exception) { } if (mex != null) { targetObjFE.SetResourceReference( AddBindingTo(targetObjFE, result), mex.ResourceKey); continue; } } // fallback result.Bindings.Add( new Binding() { Mode = BindingMode.OneWay, Source = v }); } return result.ProvideValue(serviceProvider); targetObj获得的是IProvideValueTarget。我试图通过将嵌套绑定合并到外部绑定([1a][2a])(将多绑定溢出添加到外部绑定)来解决这个问题,这可能适用于嵌套的多绑定和格式扩展,但是静态因嵌套动态资源而失败。

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


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

    无论如何,我感谢任何参与讨论的人。提出的部分解决方案非常复杂,值得更多投票。


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

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

4 个答案:

答案 0 :(得分: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>
我稍后会想到别的东西。

答案 1 :(得分: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。

我是根据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;

答案 2 :(得分:1)

我想我刚刚解决了在运行时彻底改变文化的老问题。

enter image description here enter image description here enter image description here

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

  1. 我们接受您需要DynamicResources进行本地化并编写标记扩展,这几乎是您尝试过的并且似乎很难实现。
  2. 我们只使用StaticResources,在这种情况下,绑定世界变得更加容易,但更新已绑定的字符串变得更加棘手。
  3. 我建议后者。基本上我的想法是使用resx文件的代理,它可以在文化发生变化时更新所有绑定。这个article by OlliFromTor在提供实施方面走了很长的路。

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

    Resx结构:

    enter image description here

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

    enter image description here

    enter image description here

    enter image description here

    的Xaml:

    <UserControl x:Class="WpfApplication1.Controls.LoginView"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:props="clr-namespace:WpfApplication1.Properties"
                 xmlns:models="clr-namespace:WpfApplication1.Models"
                 Background="#FCF197" 
                 FontFamily="Segoe UI"
                 TextOptions.TextFormattingMode="Display"> <!-- please notice the effect of this on font fuzzyness -->
    
        <UserControl.DataContext>
            <models:LoginViewModel />
        </UserControl.DataContext>
        <UserControl.Resources>
            <Thickness x:Key="StdMargin">5,2</Thickness>
            <Style TargetType="{x:Type TextBlock}">
                <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
            </Style>
            <Style TargetType="{x:Type Button}">
                <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
                <Setter Property="MinWidth" Value="80"/>
            </Style>
            <Style TargetType="{x:Type TextBox}">
                <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
            </Style>
            <Style TargetType="{x:Type ComboBox}">
                <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
            </Style>
    
        </UserControl.Resources>
    
        <Grid Margin="30" Height="150" Width="200">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*" MinWidth="120"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
    
            <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Username, Source={StaticResource Resx}}" />
            <TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Password, Source={StaticResource Resx}}" />
            <TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding Language, Source={StaticResource Resx}}" />
            <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbxUsername" Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" />
            <TextBox Grid.Row="1" Grid.Column="1" x:Name="tbxPassword" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}" />
            <ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Cultures}" DisplayMemberPath="DisplayName" SelectedItem="{Binding SelectedCulture}" />
            <TextBlock Grid.Row="3" Grid.ColumnSpan="2" Foreground="Blue" TextWrapping="Wrap" Margin="5,15,5,2">
                <TextBlock.Text>
                    <MultiBinding StringFormat="{x:Static props:Resources.LoginMessage}">
                        <Binding Path="Username" />
                        <Binding Path="Password" />
                        <Binding Path="Language" Source="{StaticResource Resx}" />
                        <Binding Path="SelectedCulture.DisplayName" FallbackValue="(not set)" />
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </Grid>
    </UserControl>
    

    我选择将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;
            }
        }
    }
    

答案 3 :(得分:1)

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

enter image description here enter image description here

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

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

的Xaml:

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

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

TextBlockComplex:

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

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

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

        private EventHandler _valueChangedHandler;
        private NotifyCollectionChangedEventHandler _valuesChangedHandler;

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

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

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

            this.Loaded += TextBlockComplex_Loaded;
        }

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

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

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

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

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

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

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

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

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

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

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

StringFormatContainer:

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

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

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

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

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

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

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

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

ExpiryViewModel:

using System;
using System.ComponentModel;

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

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

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

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

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