Я пытаюсь сделать 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
будет вызываться только один раз (за исключением шаблонов, которые являются отдельной историей), поэтому следует использовать другую стратегию, если фактическая значение не является постоянным (например, для Binding
и т.д.).
Я просмотрел реализацию Binding
в Reflector, его метод ProvideValue
фактически возвращает не реальный целевой объект, а экземпляр класса System.Windows.Data.BindingExpression
, который, кажется, выполняет всю реальную работу. То же самое касается DynamicResource
: он просто возвращает экземпляр System.Windows.ResourceReferenceExpression
, который заботится о подписке на (внутренний) InheritanceContextChanged
и недействительности значения, когда это необходимо. То, что я, однако, не мог понять, просматривая код, выглядит следующим образом:
- Как получается, что объект типа
BindingExpression
/ResourceReferenceExpression
не обрабатывается как "есть", но запрашивается базовое значение? - Как
MultiBindingExpression
знает, что значения базовых привязок изменились, поэтому он также должен лишить его значения?
Я действительно нашел реализацию библиотеки расширений разметки, которая утверждает, что поддерживает конкатенирование строк (что идеально подходит для моего использования) (проект, code, реализация конкатенации, полагаясь на другой код), но он, похоже, поддерживает вложенные расширения только типов библиотек (т.е. внутри не существует гнездо ваниль Binding
).
Есть ли способ реализовать синтаксис, представленный в начале вопроса? Это поддерживаемый сценарий, или это можно сделать только изнутри инфраструктуры WPF (поскольку 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
отслеживает его дочерние элементы и обновляет их свойства зависимостей и сохраняет обновленный результат в Value
), но этот синтаксис кажется уродливым, и я хочу избавиться от вспомогательных элементов в визуальное дерево.
Конечная цель состоит в том, чтобы облегчить перевод: строки UI, такие как "15 секунд до взрыва", естественно представлены в виде локализуемого формата "{0} до взрыва" (который переходит в ResourceDictionary
и будет заменен при изменении языка ) и Binding
к свойству зависимости VM, представляющему время.
Обновить отчет. Я попытался реализовать расширение разметки самостоятельно со всей информацией, которую я мог найти в Интернете. Полная реализация здесь ([1], [2], [3]), вот основная часть:
var result = new MultiBinding()
{
Converter = new StringFormatConverter(),
Mode = BindingMode.OneWay
};
foreach (var v in values)
{
if (v is MarkupExtension)
{
var b = v as Binding;
if (b != null)
{
result.Bindings.Add(b);
continue;
}
var bb = v as BindingBase;
if (bb != null)
{
targetObjFE.SetBinding(AddBindingTo(targetObjFE, result), bb);
continue;
}
}
if (v is System.Windows.Expression)
{
DynamicResourceExtension mex = null;
// didn't find other way to check for dynamic resource
try
{
// rrc is a new ResourceReferenceExpressionConverter();
mex = (MarkupExtension)rrc.ConvertTo(v, typeof(MarkupExtension))
as DynamicResourceExtension;
}
catch (Exception)
{
}
if (mex != null)
{
targetObjFE.SetResourceReference(
AddBindingTo(targetObjFE, result),
mex.ResourceKey);
continue;
}
}
// fallback
result.Bindings.Add(
new Binding() { Mode = BindingMode.OneWay, Source = v });
}
return result.ProvideValue(serviceProvider);
Это, похоже, работает с привязками вложений и динамическими ресурсами, но с треском проваливается, пытаясь вложить его в себя, так как в этом случае targetObj
, полученный из IProvideValueTarget
, null
. Я попытался обойти это, объединив вложенные привязки во внешний ([1a], [2a]) (добавлено многосвязное разлитие во внешнее связывание), возможно, это работало бы с вложенными многосвязными и расширением формата, но все еще не удается с вложенными динамическими ресурсами.
Интересно, что при развёртывании разного рода расширений разметки я получаю Binding
и MultiBinding
во внешнем расширении, но ResourceReferenceExpression
вместо DynamicResourceExtension
. Интересно, почему это непоследовательно (и как Binding
реконструирован из BindingExpression
).
Обновить отчет: к сожалению, идеи, приведенные в ответах, не привели к решению проблемы. Возможно, это доказывает, что расширения разметки, будучи достаточно мощным и универсальным инструментом, нуждаются в большем внимании со стороны команды WPF.
В любом случае я благодарю всех, кто принимал участие в обсуждении. Частичные решения, которые были представлены, достаточно сложны, чтобы заслуживать большего внимания.
Обновить отчет: похоже, нет хорошего решения с расширениями разметки или, по крайней мере, уровень знаний WPF, необходимых для его создания, слишком глубок, чтобы быть практичным.
Тем не менее, у @adabyron появилась идея улучшения, которая помогает скрыть вспомогательные элементы в элементе хоста (цена этого, однако, подклассифицирует хост). Я постараюсь выяснить, возможно ли избавиться от подкласса (используя поведение, которое захватывает хост LogicalChildren и добавляет к нему вспомогательные элементы, в основе которого лежит старая версия того же ответа).