我试图让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
并在适当时使值无效。通过查看代码我无法理解的是:
InheritanceContextChanged
/ BindingExpression
类型的对象未被处理&#34;按原样#34;但是被要求提供基础值?ResourceReferenceExpression
如何知道底层绑定的值已经改变,所以它也必须使其值无效?我实际上发现了一个标记扩展库实现,声称支持连接字符串(完全映射到我的用例)(project,code,concatenation 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])(将多绑定溢出添加到外部绑定)来解决这个问题,这可能适用于嵌套的多绑定和格式扩展,但是静态因嵌套动态资源而失败。
有趣的是,在嵌套不同类型的标记扩展时,我会在外部扩展中获得null
和Binding
,但MultiBinding
而不是ResourceReferenceExpression
。我想知道为什么它不一致(DynamicResourceExtension
如何从Binding
重建。
更新报告:遗憾的是,答案中提供的想法并未解决问题。也许它证明了标记扩展虽然是功能强大且功能多样的工具,但需要WPF团队更多关注。
无论如何,我感谢任何参与讨论的人。提出的部分解决方案非常复杂,值得更多投票。
更新报告:似乎没有标记扩展的好解决方案,或者至少创建一个所需的WPF知识水平太深而不实用。
然而,@ adabyron有一个改进的想法,这有助于隐藏主机项中的辅助元素(但是这个价格是主机的子类)。我试着看看是否有可能摆脱子类化(使用劫持主机的LogicalChildren并添加辅助元素的行为出现在我脑海中,受旧版本的启发同样的答案)。
答案 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)
我想我刚刚解决了在运行时彻底改变文化的老问题。
我看到它的方式有两种可能性:
我建议后者。基本上我的想法是使用resx文件的代理,它可以在文化发生变化时更新所有绑定。这个article by OlliFromTor在提供实施方面走了很长的路。
对于更深层次的嵌套,StringFormat不接受绑定的限制,因此如果StringFormats不能保持静态,您可能仍需要引入转换器。
Resx结构:
Resx内容(默认/不/ es):
的Xaml:
<UserControl x:Class="WpfApplication1.Controls.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:props="clr-namespace:WpfApplication1.Properties"
xmlns:models="clr-namespace:WpfApplication1.Models"
Background="#FCF197"
FontFamily="Segoe UI"
TextOptions.TextFormattingMode="Display"> <!-- please notice the effect of this on font fuzzyness -->
<UserControl.DataContext>
<models:LoginViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<Thickness x:Key="StdMargin">5,2</Thickness>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="{StaticResource StdMargin}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin" Value="{StaticResource StdMargin}"/>
<Setter Property="MinWidth" Value="80"/>
</Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Margin" Value="{StaticResource StdMargin}"/>
</Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="Margin" Value="{StaticResource StdMargin}"/>
</Style>
</UserControl.Resources>
<Grid Margin="30" Height="150" Width="200">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="120"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Username, Source={StaticResource Resx}}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Password, Source={StaticResource Resx}}" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding Language, Source={StaticResource Resx}}" />
<TextBox Grid.Row="0" Grid.Column="1" x:Name="tbxUsername" Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Grid.Row="1" Grid.Column="1" x:Name="tbxPassword" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Cultures}" DisplayMemberPath="DisplayName" SelectedItem="{Binding SelectedCulture}" />
<TextBlock Grid.Row="3" Grid.ColumnSpan="2" Foreground="Blue" TextWrapping="Wrap" Margin="5,15,5,2">
<TextBlock.Text>
<MultiBinding StringFormat="{x:Static props:Resources.LoginMessage}">
<Binding Path="Username" />
<Binding Path="Password" />
<Binding Path="Language" Source="{StaticResource Resx}" />
<Binding Path="SelectedCulture.DisplayName" FallbackValue="(not set)" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</UserControl>
我选择将ResourcesProxy的实例添加到App.xaml,还有其他可能性(例如直接在ViewModel上实例化和公开代理)
<Application x:Class="WpfApplication1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:props="clr-namespace:WpfApplication1.Properties"
StartupUri="MainWindow.xaml">
<Application.Resources>
<props:ResourcesProxy x:Key="Resx" />
</Application.Resources>
</Application>
视图模型:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Threading;
using System.Windows;
using WpfApplication1.Properties;
namespace WpfApplication1.Models
{
public class LoginViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
if (propertyName == "SelectedCulture")
ChangeCulture();
}
private ObservableCollection<CultureInfo> _cultures;
public ObservableCollection<CultureInfo> Cultures { get { return _cultures; } set { _cultures = value; OnPropertyChanged("Cultures"); } }
private CultureInfo _selectedCulture;
public CultureInfo SelectedCulture { get { return _selectedCulture; } set { _selectedCulture = value; OnPropertyChanged("SelectedCulture"); } }
private string _username;
public string Username { get { return _username; } set { _username = value; OnPropertyChanged("Username"); } }
private string _password;
public string Password { get { return _password; } set { _password = value; OnPropertyChanged("Password"); } }
public LoginViewModel()
{
this.Cultures = new ObservableCollection<CultureInfo>()
{
new CultureInfo("no"),
new CultureInfo("en"),
new CultureInfo("es")
};
}
private void ChangeCulture()
{
Thread.CurrentThread.CurrentCulture = this.SelectedCulture;
Thread.CurrentThread.CurrentUICulture = this.SelectedCulture;
var resx = Application.Current.Resources["Resx"] as ResourcesProxy;
resx.ChangeCulture(this.SelectedCulture);
}
}
}
最后重要的部分是ResourcesProxy:
using System.ComponentModel;
using System.Dynamic;
using System.Globalization;
using System.Linq;
using System.Reflection;
namespace WpfApplication1.Properties
{
/// <summary>
/// Proxy to envelop a resx class and attach INotifyPropertyChanged behavior to it.
/// Enables runtime change of language through the ChangeCulture method.
/// </summary>
public class ResourcesProxy : DynamicObject, INotifyPropertyChanged
{
private Resources _proxiedResources = new Resources(); // proxied resx
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(_proxiedResources, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Sets the new culture on the resources and updates the UI
/// </summary>
public void ChangeCulture(CultureInfo newCulture)
{
Resources.Culture = newCulture;
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(null));
}
private PropertyInfo GetPropertyInfo(string propertyName)
{
return _proxiedResources.GetType().GetProperties().First(pi => pi.Name == propertyName);
}
private void SetMember(string propertyName, object value)
{
GetPropertyInfo(propertyName).SetValue(_proxiedResources, value, null);
OnPropertyChanged(propertyName);
}
private object GetMember(string propertyName)
{
return GetPropertyInfo(propertyName).GetValue(_proxiedResources, null);
}
public override bool TryConvert(ConvertBinder binder, out object result)
{
if (binder.Type == typeof(INotifyPropertyChanged))
{
result = this;
return true;
}
if (_proxiedResources != null && binder.Type.IsAssignableFrom(_proxiedResources.GetType()))
{
result = _proxiedResources;
return true;
}
else
return base.TryConvert(binder, out result);
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = GetMember(binder.Name);
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
SetMember(binder.Name, value);
return true;
}
}
}
答案 3 :(得分:1)
查看以下内容是否适合您。我接受了你在评论中提供的test case并略微扩展它以更好地说明机制。我想关键是通过在嵌套容器中使用DependencyProperties
来保持灵活性。
编辑:我已使用TextBlock的子类替换了混合行为。这为DataContext和DynamicResources添加了更容易的链接。
在旁注中,您的项目使用DynamicResources
引入条件的方式不是我建议的。而是尝试使用ViewModel来建立条件,和/或使用触发器。
的Xaml:
<UserControl x:Class="WpfApplication1.Controls.ExpiryView" xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:props="clr-namespace:WpfApplication1.Properties" xmlns:models="clr-namespace:WpfApplication1.Models"
xmlns:h="clr-namespace:WpfApplication1.Helpers" xmlns:c="clr-namespace:WpfApplication1.CustomControls"
Background="#FCF197" FontFamily="Segoe UI"
TextOptions.TextFormattingMode="Display"> <!-- please notice the effect of this on font fuzzyness -->
<UserControl.DataContext>
<models:ExpiryViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<system:String x:Key="ShortOrLongDateFormat">{0:d}</system:String>
</UserControl.Resources>
<Grid>
<StackPanel>
<c:TextBlockComplex VerticalAlignment="Center" HorizontalAlignment="Center">
<c:TextBlockComplex.Content>
<h:StringFormatContainer StringFormat="{x:Static props:Resources.ExpiryDate}">
<h:StringFormatContainer.Values>
<h:StringFormatContainer Value="{Binding ExpiryDate}" StringFormat="{DynamicResource ShortOrLongDateFormat}" />
<h:StringFormatContainer Value="{Binding SecondsToExpiry}" />
</h:StringFormatContainer.Values>
</h:StringFormatContainer>
</c:TextBlockComplex.Content>
</c:TextBlockComplex>
</StackPanel>
</Grid>
</UserControl>
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();
}
}
}