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 DynamicResource
s.
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:
- How does it happen that the object of type
BindingExpression
/ResourceReferenceExpression
is not treated "as is", but is asked for the underlying value? - 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 Binding
s and MultiBinding
s 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).
I think I just solved the old problem of switching culture at runtime quite neatly.
The way I see it, there are two possibilities:
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:
Resx contents (default/no/es):
Xaml:
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)
ViewModel:
And finally the important part, the ResourcesProxy:
you can combine the use of Binding with Resources as well as Properties :
Sample :
XAML :
CS :
Edit :
here's a work around for now
i'll think of something else later .
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.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:
TextBlockComplex:
StringFormatContainer:
ExpiryViewModel:
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.
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:
the extension class:
The 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:
The only drawback I encountered is that, when changing the values in the
stringlist
, the resource has to be reassigned: