String format with a markup extension
Asked Answered
P

4

13

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

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

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

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

with obvious implementation of StringFormatConveter.

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

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

or maybe just

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

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

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

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

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

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

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


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

<l:FormatHelper x:Name="h1" Format="{DynamicResource FORMAT_ID'">
    <l:FormatArgument Value="{Binding Data1}"/>
    <l:FormatArgument Value="{StaticResource Data2}"/>
</l:FormatHelper>
<TextBlock Text="{Binding Value, ElementName=h1}"/>

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


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


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

var result = new MultiBinding()
{
    Converter = new StringFormatConverter(),
    Mode = BindingMode.OneWay
};

foreach (var v in values)
{
    if (v is MarkupExtension)
    {
        var b = v as Binding;
        if (b != null)
        {
            result.Bindings.Add(b);
            continue;
        }

        var bb = v as BindingBase;
        if (bb != null)
        {
            targetObjFE.SetBinding(AddBindingTo(targetObjFE, result), bb);
            continue;
        }
    }

    if (v is System.Windows.Expression)
    {
        DynamicResourceExtension mex = null;
        // didn't find other way to check for dynamic resource
        try
        {
            // rrc is a new ResourceReferenceExpressionConverter();
            mex = (MarkupExtension)rrc.ConvertTo(v, typeof(MarkupExtension))
                as DynamicResourceExtension;
        }
        catch (Exception)
        {
        }
        if (mex != null)
        {
            targetObjFE.SetResourceReference(
                    AddBindingTo(targetObjFE, result),
                    mex.ResourceKey);
            continue;
        }
    }

    // fallback
    result.Bindings.Add(
        new Binding() { Mode = BindingMode.OneWay, Source = v });
}

return result.ProvideValue(serviceProvider);

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

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


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

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


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

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

Peptone answered 20/8, 2014 at 22:19 Comment(1)
Every time iv'e ever created a Custom MarkupExtension i ended up removing it and finding a simpler way of doing what ever it was that needed to be done , conveters , attached properties , there are allot of easy to use Technics..Karinekariotta
S
1

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

enter image description here enter image description here

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

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

Xaml:

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

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

TextBlockComplex:

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();
        }
    }
}
Sorgo answered 6/9, 2014 at 12:14 Comment(14)
This seems to be better, thank you! Let me try it. I wonder if the behaviours get into the visual tree so that they can update the dynamic resource bindings.Peptone
As an answer to your side not, actually the project doesn't use dynamic resources for business logic/conditions. We use e. g. application-wide UserPreferredDateFormat instead of ShortOrLongDateFormat, so that we don't need to propagate it to all and every single VM.Peptone
Well, I've tried your solution. It works well with your example, and I really like the technique (extending logical tree is particularly nice!). Unfortunately it doesn't pick up the changes of DynamicResources. This is because the behaviors are not a part of the outer logical tree, and thus don't get the updates of the outer dynamic resources. This is as well the reason for not inheriting DataContext, and perhaps the reason for not activating the inner bindings as well. Anyway, thank you for your attention to this problem!Peptone
I suppose this is a problem that can be overcome. What is the situation, are you swapping whole dictionaries, or changing single resources by key? For the latter, the workaround is easy - you just need to make sure the relevant resources are present in the TextBindingBehavior.Content resources. E.g. in OnAttached: this.Content.Resources.MergedDictionaries.Add(Application.Current.Resources);Sorgo
Well. In the typical localization scenario, it's the merged dictionaries (on some unknown nesting depth) of Application.Current's resources that are updated. But for the scenarios like UserPreferredDateFormat, the value can be set (and overridden) at any position (not necessarily on Application/Window level) by the outer code, so I cannot rely on it being at some designated place.Peptone
Technically, we could actually do the following: declare an attached property for the target textblock (or an unlimited number of them), set it to resource reference from behaviour (which should scan all the nested StringFormatContainers), and bind to it (like it's done in the code in OP), but I am not sure this is easy. (Another problem is perhaps dependency property leak: we need to ensure the number of them doesn't grow to infinity if the binding is recreated by e. g. trigger. But this is another can of worms in itself.)Peptone
I've updated TextBindingBehavior so it generically adds the relevant ResourceDictionaries to the StringFormatContainers... see if it works for you. However, it's done with reflection, and it's a hack. I don't like the construct, and I would strongly suggest you choose to change your project instead. UserPreferredDataFormat sounds to me like an application-wide user configuration, which should be a property of a settings class instead of a DynamicResource. If it isn't application-wide, and you need a different values in different locations, use additional setting properties to bind to.Sorgo
I don't really think there is such thing as "relevant dictionaries". Resources can be overridden at any time on any level, so there should be no static way of getting them. (E.g., the item can be created in some usercontrol and added to some window-rooted logical tree later.) The solution you presented will work in some (static) scenarios but won't in other ones. And the solution seems to be syntactically not much better than this one (mentioned in OP), which doesn't suffer from the problem with dynamic resources.Peptone
Why don't you code the scenario that does not work and present it to me, so I can solve it. And unless I missed something you had not solved the nesting with the ugly invisible UI helper elements that my code got rid of, and you requested to be rid of. The relevant resource dictionaries are obviously the ones that contain the DynamicResources used for the StringFormatContainers.Sorgo
Check out if you like the subclass better.Sorgo
Well, invisible helpers are ugly, but syntactically they are not much different from the presented solution. An advantage of helper elements is that you can e. g. bind the text to label or whatever, and not just a textblock. But this is already technical detail.Peptone
I tried the solution with a subclass; it works, even with setting an intermediate resource dictionary on non-window level. I'll try to see if it's possible to avoid subclassing a textblock with an attached property or Freezable trick. As far as I understand, StringFormatContainer is the same as in FormatHelper, but it resides in the host item.Peptone
The scenario that didn't work with attached behavior and works with inserting StringFormatContainer into the logical tree is like this: the dynamic resource set at application level is at some moment overridden by some local resource value (for example, set at window or inner control level). The old code didn't peek up the changes.Peptone
Well. So back to the original question, it seems that the markup extensions are not an appropriate tool, as the way Binding works is too magic and undocumented. Your solution in this answer seems to be the only one having advantage over the solution presented in the question. Thanks a lot for your patience and your insight!Peptone
K
3

you can combine the use of Binding with Resources as well as Properties :

Sample :

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

Edit :

here's a work around for now

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

i'll think of something else later .

Karinekariotta answered 20/8, 2014 at 23:35 Comment(4)
Not sure if this is going to work with DynamicResource as well. (I'll check it as soon as I get to the compiler.) For localization, DynamicResource is a primary scenario.Peptone
Unfortunately, it doesn't work. Replacing StaticResource with DynamicResource produces run-time exception: A 'DynamicResourceExtension' cannot be set on the 'Source' property of type 'Binding'. A 'DynamicResourceExtension' can only be set on a DependencyProperty of a DependencyObject.Peptone
About workaround: I thought about it, but (1) Tag can be already used for something different, (2) it allows only one DynamicResource per UI object. This can of course be worked around by declaring a "sufficient" number of attached properties, but than the solution is not really better than the last code snippet in the question. (The trick is to define enough DPs and use them as bridge between DynamicResource and Binding.)Peptone
What i was thinking is for one DP which it's type is a collection of resources , i need to look into how this can be done.Karinekariotta
O
3

I know I'm not exactly answering your question, but there is already a mechanism in wpf that allows for string formatting in xaml, it is BindingBase.StringFormat property

I haven't figured out how to make it work with DynamicResource binding, but it works with other bindings, such as binding to the property of data context, to static resource or to the property of another element.

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

If you really want to implement your own markup extension that takes a binding, there is a way. I implemented a markup extension that takes a name of a picture (or a binding to something that holds it) as a constructor argument, then resolves the path and returns the ImageSource.

I implemented it based on this artcle.

Since I'm bad at explaining, I better illustrate it using code:

<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}" />

the extension class:

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

The converter:

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

EDIT:

I updated the ImgSourceExtension so now it will work with StaticResource and DynamicResource, although I still don't know how to do the sort of nested binding the OP is looking for.

Having said that, during my research yesterday I stumbled upon an interesting "hack" related to binding to dynamic resources. I think combining it with a SortedList or another collection data type that can be accessed by key may be worth looking into:

 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>

The only drawback I encountered is that, when changing the values in the stringlist, the resource has to be reassigned:

  SortedList newresource = new SortedList(((SortedList)Resources["stringlist"]));
  newresource["key0"] = "1234";
  this.Resources["stringlist"] = newresource;
Ophthalmoscope answered 28/8, 2014 at 12:22 Comment(3)
Well, the problem with StringFormat is not a dependency property (since the MultiBinding itself is not a DependencyObject), and MultiBinding doesn't do any tricks to accept dependency properties there. Moreover, I don't think that using DynamicResources is possible on this way (except as with declaring additional dependency properties, it it was already discussed in the comments to @eran's answer).Peptone
@Peptone yesterday I found a way to use dynamic resource as datasource of control, and then do multibinding. I updated my answer.Ophthalmoscope
The updated version of ImgSourceExtension deals with DynamicResource, but it resolves the resource immediately (targetObject.TryFindResource), so this means that if the resource value changes later, the new value won't be picked up. (This is critical for localization, which is my primary target.) About the trick with DataContext, it's essentially the same as the trick with Tag proposed by @eran, and has the same problems: it doesn't work if DataContext is used for something different, or if we need more than one DynamicResource in the expression.Peptone
S
1

I think I just solved the old problem of switching culture at runtime quite neatly.

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

The way I see it, there are two possibilities:

  1. We accept that you will need DynamicResources for your localization and write a markup extension, which is pretty much what you have tried and seems hard to achieve.
  2. We just use StaticResources, in which case the world of bindings becomes much easier, yet updating already bound strings becomes trickier.

I suggest the latter. Basically my idea is to use a proxy to the resx file which is able to update all bindings once the culture changes. This article by OlliFromTor went a long way towards providing the implementation.

For deeper nesting, there's the limitation that StringFormat does not accept bindings, so you might still have to introduce a converter if the StringFormats cannot be kept static.

Resx structure:

enter image description here

Resx contents (default/no/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>

I chose to add the instance of the ResourcesProxy to App.xaml, there are other possibilities (e.g. instantiating and exposing the proxy directly on the 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>

ViewModel:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Threading;
using System.Windows;
using WpfApplication1.Properties;

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

            if (propertyName == "SelectedCulture")
                ChangeCulture();
        }

        private ObservableCollection<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);
        }
    }
}

And finally the important part, the 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;
        }
    }
}
Sorgo answered 31/8, 2014 at 15:54 Comment(7)
Well, I see two points here. First, inclusion of DynamicResources to the markup extension's data sources is what I am ultimately trying to achieve. I am aware about the other solutions, but the application I need this feature for has chosen ResourceDicitonary-based localization. Aside of that, the DynamicResources can come from other sources, not necessary from localization needs, and they need to be supported as well. Using MultiBinding, as proposed in your answer, doesn't help when nesting is needed: it doesn't accept other MultiBinding as data sources as well.Peptone
Second, I don't see the need of DynamicObject: if I know at compile time, which exactly type is going to be proxied, I can define the properties myself and don't need all the power (and performance penalty) of DynamicObject.Peptone
First, as I've stated, for deeper nesting a converter will probably be necessary. Can you provide a (nested) example that should be solved? Your ultimate goal talks about translation, what other DynamicResources except strings that can be easily relocated into resx files will you use? Second, DynamicObject provides the exact mechanism that is needed to have an automatic proxy to the members of the base class, with the methods of TryGet/SetMember in which PropertyChanged can be raised.Sorgo
Well, I didn't find a way to do the nesting with a converter. The ultimate goal is translation, but DynamicResource can be used for other (not translation-related) things. You can supply any value to a UserControl via setting a resource into the outer ResourceDictionary, so the resource value depends not on language, but on other program logic.Peptone
This idea (adding an intermediate object which listens to language change and reports property change) seems to be simpler and not require DLR. Anyway, the translation in the project I am working at is done not through resx.Peptone
Simpler, seriously?! You seem to be extremely reluctant to use DLR, and yet you use DynamicResources without further ado. I've provided another solution. Involving DynamicResources, and no resx.Sorgo
Well, DLR is a little bit too powerful, so I'd like better to avoid it if possible. And I don't actually like the idea that the dynamic resources should be avoided, because they are useful not only for translation purpose. So this mean that (1) we cannot use DynamicResource with localization, (2) we must supply another class which does roughly what DynamicResources do (but not so versatile), (3) we must educate the developers to special case localized strings (so they must know which of them are localized in advance).Peptone
S
1

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

enter image description here enter image description here

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

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

Xaml:

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

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

TextBlockComplex:

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();
        }
    }
}
Sorgo answered 6/9, 2014 at 12:14 Comment(14)
This seems to be better, thank you! Let me try it. I wonder if the behaviours get into the visual tree so that they can update the dynamic resource bindings.Peptone
As an answer to your side not, actually the project doesn't use dynamic resources for business logic/conditions. We use e. g. application-wide UserPreferredDateFormat instead of ShortOrLongDateFormat, so that we don't need to propagate it to all and every single VM.Peptone
Well, I've tried your solution. It works well with your example, and I really like the technique (extending logical tree is particularly nice!). Unfortunately it doesn't pick up the changes of DynamicResources. This is because the behaviors are not a part of the outer logical tree, and thus don't get the updates of the outer dynamic resources. This is as well the reason for not inheriting DataContext, and perhaps the reason for not activating the inner bindings as well. Anyway, thank you for your attention to this problem!Peptone
I suppose this is a problem that can be overcome. What is the situation, are you swapping whole dictionaries, or changing single resources by key? For the latter, the workaround is easy - you just need to make sure the relevant resources are present in the TextBindingBehavior.Content resources. E.g. in OnAttached: this.Content.Resources.MergedDictionaries.Add(Application.Current.Resources);Sorgo
Well. In the typical localization scenario, it's the merged dictionaries (on some unknown nesting depth) of Application.Current's resources that are updated. But for the scenarios like UserPreferredDateFormat, the value can be set (and overridden) at any position (not necessarily on Application/Window level) by the outer code, so I cannot rely on it being at some designated place.Peptone
Technically, we could actually do the following: declare an attached property for the target textblock (or an unlimited number of them), set it to resource reference from behaviour (which should scan all the nested StringFormatContainers), and bind to it (like it's done in the code in OP), but I am not sure this is easy. (Another problem is perhaps dependency property leak: we need to ensure the number of them doesn't grow to infinity if the binding is recreated by e. g. trigger. But this is another can of worms in itself.)Peptone
I've updated TextBindingBehavior so it generically adds the relevant ResourceDictionaries to the StringFormatContainers... see if it works for you. However, it's done with reflection, and it's a hack. I don't like the construct, and I would strongly suggest you choose to change your project instead. UserPreferredDataFormat sounds to me like an application-wide user configuration, which should be a property of a settings class instead of a DynamicResource. If it isn't application-wide, and you need a different values in different locations, use additional setting properties to bind to.Sorgo
I don't really think there is such thing as "relevant dictionaries". Resources can be overridden at any time on any level, so there should be no static way of getting them. (E.g., the item can be created in some usercontrol and added to some window-rooted logical tree later.) The solution you presented will work in some (static) scenarios but won't in other ones. And the solution seems to be syntactically not much better than this one (mentioned in OP), which doesn't suffer from the problem with dynamic resources.Peptone
Why don't you code the scenario that does not work and present it to me, so I can solve it. And unless I missed something you had not solved the nesting with the ugly invisible UI helper elements that my code got rid of, and you requested to be rid of. The relevant resource dictionaries are obviously the ones that contain the DynamicResources used for the StringFormatContainers.Sorgo
Check out if you like the subclass better.Sorgo
Well, invisible helpers are ugly, but syntactically they are not much different from the presented solution. An advantage of helper elements is that you can e. g. bind the text to label or whatever, and not just a textblock. But this is already technical detail.Peptone
I tried the solution with a subclass; it works, even with setting an intermediate resource dictionary on non-window level. I'll try to see if it's possible to avoid subclassing a textblock with an attached property or Freezable trick. As far as I understand, StringFormatContainer is the same as in FormatHelper, but it resides in the host item.Peptone
The scenario that didn't work with attached behavior and works with inserting StringFormatContainer into the logical tree is like this: the dynamic resource set at application level is at some moment overridden by some local resource value (for example, set at window or inner control level). The old code didn't peek up the changes.Peptone
Well. So back to the original question, it seems that the markup extensions are not an appropriate tool, as the way Binding works is too magic and undocumented. Your solution in this answer seems to be the only one having advantage over the solution presented in the question. Thanks a lot for your patience and your insight!Peptone

© 2022 - 2024 — McMap. All rights reserved.