Ситуацию усугубляет MVVM, благодаря которому не использовать это чудо науки получается довольно редко. Что-же, пришло время немного облегчить рутину создания и использования конвертеров, поехали.
Сразу оговорюсь, что я не против часто используемых конвертеров типа BooleanToVisibilityConverter и тому подобных, их вполне можно запомнить и переиспользовать во многих местах. Но часто бывает так, что конвертер нужен какой-то очень специфический, и делать из него целый компонент как-то не хочется. И долго это, и засоряет глобальную область видимости, потом сложно выискивать нужное во всем этом мусоре.
Конвертеры используются при работе с binding-ами и позволяют преобразовывать значения в одностороннем или двустороннем порядке (В зависимости от режима биндинга). Конвертеры также бывают двух типов — с одним значением и со множеством. За них отвечают интерфейсы IValueConverter и IMultiValueConverter соответственно.
При одиночном значении мы используем обычные биндинги, обычно через встроенное в XAML расширение разметки BindingBase:
<TextBlock Text="{Binding IntProp, Converter={StaticResource conv:IntToStringConverter}, ConverterParameter=plusOne}" />
В случае мульти-значения, используется такая монструозная конструкция:
<TextBlock>
<TextBlock.Text>
<MultiBinding
Converter="{StaticResource conv:IntToStringConverter}"
ConverterParameter="plusOne">
<Binding Path="IntProp" />
<Binding Path="StringProp" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Сами конвертеры же будут выглядеть так (Тут сразу два конвертера в одном классе, но можно и по-отдельности):
public class IntToStringConverter : IValueConverter, IMultiValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> (string)parameter == "plusOne" ? ((int)value + 1).ToString() : value.ToString();
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
=> $"{Convert(values[0], targetType, parameter, culture) as string} {values[1]}";
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
Это крайне коротко, так как пример синтетический, но уже тут без пол литра ни черта не понятно, что за массивы, что за приведения типов, какие-то левые targetType и culture, что за ConvertBack без реализации.
Идей по упрощению этого у меня несколько:
- Конвертеры в виде кусков c# кода прямо в xaml для простых вычислений;
- Конвертеры в виде ссылок на методы в code-behind, для случаев с очень конкретными/частными случаями конвертации, то есть тогда, когда нет смысла эту конвертацию где-либо еще переиспользовать;
- Конвертеры в виде того-же самого, что в стандартной реализации, но чтобы это не выглядело так стремно, чтобы каждый раз при написании нового конвертера не приходилось лезть в гугл и искать пример реализации конвертера.
Сразу обломаю тех из вас, кто думает, что я буду рассказывать как реализовать пункт 1. Я не буду. В сети есть несколько реализаций подобного, например тут. Также я видел варианты с expression tree и кажется еще какие-то. Такие вещи годятся только для простейших случаев — для работы с арифметическими и логическими операциями. Если же там нужно будет вызывать какие-то классы, использовать строки и так далее, то вылезут проблемы с экранированием внутри xml и проблема включения namespace-ов. Однако в простейших случаях подобные вещи использовать вполне можно.
А вот 2 и 3 пункты рассмотрим поподробнее. Допустим, понадобилось определить метод конвертации в code-behind. Как это должно выглядеть? Я думаю что примерно так:
private string ConvertIntToString(int intValue, string options)
=> options == "plusOne" ? (intValue + 1).ToString() : intValue.ToString();
private string ConvertIntAndStringToString(int intValue, string stringValue, string options)
=> $"{ConvertIntToString(intValue, options)} {stringValue}";
Сравните это с предыдущим вариантом. Кода меньше — ясности больше. Аналогично может выглядеть вариант с отдельным переиспользуемым конвертером:
public static class ConvertLib
{
public static string IntToString(int intValue, string options)
=> options == "plusOne" ? (intValue + 1).ToString() : intValue.ToString();
public static string IntAndStringToString(int intValue, string stringValue, string options)
=> $"{IntToString(intValue, options)} {stringValue}";
}
Неплохо, да? Хорошо, а как подружить xaml с этим, ведь он понимает только стандартные интерфейсы конвертеров? Можно конечно для каждого такого класса делать обертку в виде стандартных IValueConverter/IMultiValueConverter которая будет уже использовать красивые методы, но тогда теряется весь смысл, если придется объявлять по обертке на каждый читабельный конвертер. Одно из решений — сделать подобную обертку универсальной, типа такой:
public class GenericConverter : IValueConverter, IMultiValueConverter
{
public GenericConverter(/* Описания методов конвертации в виде делегатов или еще как-то */)
{
// Сохраняем методы конвертации
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Вызываем конвертацию по делегату, преобразуя входные параметры
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// Вызываем конвертацию по делегату, преобразуя входные параметры
}
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// Вызываем конвертацию по делегату, преобразуя входные параметры
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
// Вызываем конвертацию по делегату, преобразуя входные параметры
}
}
Это все в теории, как же практически передать в конвертер делегаты, и как их взять имея только XAML?
На помощь приходит механизм расширения разметки, MarkupExtension. Достаточно унаследовать класс MarkupExtension и переопределить метод ProvideValue и в XAML можно будет писать Binding-подобные выражения в фигурных скобочках, но со своими механизмами работы.
Для того, чтобы через расширения разметки передать ссылку на методы конвертации — самое простое, это использовать их строковые названия. Условимся что code-behind методы будем определять просто названием метода, а статические методы во внешних библиотеках будут идти вида ClrNamespace.ClassName.MethodName, отличить их можно будет по наличию точки у последнего (Как минимум одна точка будет между названием класса и метода, если класс лежит в глобальном пространстве имен).
Как идентифицировать методы разобрались, как же их получить в расширении разметки в виде делегатов, чтобы передать в конвертер? Расширение разметки (MarkupExtension) имеет метод ProvideValue для переопределения, который выглядит так:
public class GenericConvExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
// Какой-то код
}
}
Переопределяемый метод должен вернуть то, что в итоге присвоится в то свойство, в значении которого в XAML разметке определяется это расширение разметки. Метод этот может вернуть любое значение, но так как мы это расширение разметки будем подставлять в свойство Converter у биндинга (или мульти-биндинга), то возвращаемое значение должно быть конвертером, то есть экземпляром типа IValueConverter/IMultiValueConverter. Опять же, нет смысла делать разные конвертеры, можно сделать один класс и реализовать сразу два этих интерфейса, чтобы конвертер подходил как под одиночный биндинг, так и под множественный.
Для того, чтобы передать в расширение разметки строку, определяющую название функции из code-behind или статической библиотеки, которую должен вызывать конвертер, нужно определить в экземпляре MarkupExtension-а публичное строковое свойство:
public string FunctionName { get; set; }
После этого в разметке можно будет писать так:
<TextBlock Text="{Binding IntProp, Converter={conv:GenericConvExtension FunctionName='ConvertIntToString'}, ConverterParameter=plusOne}" />
Однако и это можно упростить, для начала, необязательно писать Extension в названии класса расширения conv:GenericConvExtension в XAML-е, достаточно просто conv:GenericConv. Далее в расширении можно определить конструктор, для того чтобы не указывать явно название свойства с именем функции:
public GenericConvExtension(string functionName)
{
FunctionName = functionName;
}
Теперь выражение в XAML-е стало еще проще:
<TextBlock Text="{Binding IntProp, Converter={conv:GenericConv ConvertIntToString}, ConverterParameter=plusOne}" />
Обратите также внимание на отсутствие кавычек в названии функции конвертации. В случаях, когда в строке нет пробелов и других нездоровых символов — одинарные кавычки не обязательны.
Теперь осталось только получить в методе ProvideValue ссылку на метод, создать экземпляр конвертера и передать в него эту ссылку. Ссылку на метод можно получить через механизм Reflection, однако для этого нужно знать runtime-тип, в котором объявлен этот метод. В случае с реализацией методов конвертации в статических классах передается полное имя статического метода (С указанием полного имени класса), соответственно можно распарсить эту строку, по полному имени типа через Reflection получить тип, и уже из типа также получить определение метода в виде экземпляра MethodInfo.
В случае с code-behind нужен не только тип, но и экземпляр этого типа (Ведь метод может быть не статическим и учитывать состояние Window экземпляра при выдаче результата конвертации). К счастью это не проблема, так как его можно получить через входной параметр метода ProvideValue:
public override object ProvideValue(IServiceProvider serviceProvider)
{
object rootObject = (serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider).RootObject;
// ...
}
rootObject это и будет тот объект, в котором пишется code-behind, в случае окна это будет объект Window. Вызвав у него GetType можно через рефлексию получить интересующий нас метод конвертации, так как его имя задано в определенном ранее свойстве FunctionName. Далее просто нужно создать экземпляр GenericConverter, передав в него полученный MethodInfo и вернуть этот конвертер в результате ProvideValue.
Вот и вся теория, в конце статьи я приведу код своей реализации всего этого дела. Моя реализация в строке с названием метода принимает как метод конвертации так и опционально метод обратной конвертации, синтаксис примерно такой:
Общий вид:
'[Общее_имя_статического_класса] [Имя_статического_класса.]Имя_метода_конвертации, [Имя_статического_класса.]Имя_метода_обратной_конвертации'
Пример для статической библиотеки с методами-конвертерами:
'Converters.ConvertLib IntToString, StringToInt' = 'Converters.ConvertLib.IntToString, Converters.ConvertLib.StringToInt'
Пример для code-behind:
'IntToString' для one-way binding, 'IntToString, StringToInt' для two-way binding
Смешанный вариант (прямой метод в code-behind, обратный в статической либе):
'IntToString, Converters.ConvertLib.StringToInt'
Также все это работает и с мульти-биндингами, различие будет только в сигнатуре функций для конвертации (она должна соответствовать тому, что идет в биндинге). Также ConverterParameter может присутствовать в сигнатуре функции конвертации, а может отсутствовать, для этого его просто надо указать, либо не указывать, он определяется как просто последний параметр в сигнатуре.
Пример, рассмотренный в статье в случае с моей реализацией будет выглядеть в XAML вот так:
<TextBlock Text="{Binding IntProp, Converter={conv:ConvertFunc 'ConvertIntToString'}, ConverterParameter=plusOne}" />
<TextBlock>
<TextBlock.Text>
<MultiBinding
Converter="{conv:ConvertFunc 'ConvertIntAndStringToString'}"
ConverterParameter="plusOne">
<Binding Path="IntProp" />
<Binding Path="StringProp" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Минусы моей реализации, которые я нашел:
- В момент вызова метода идут всякие проверки, создание массивов для параметров, и вообще я не уверен что MethodInfo.Invoke() работает так же быстро, как вызов метода напрямую, однако я бы не сказал что это большой минус в условиях работы с WPF/MVVM.
- Нет возможности использовать перегрузки, так как на момент получения MethodInfo неизвестны типы значений, которые будут приходить, а значит нельзя получить нужную перегрузку метода в этот момент (Возможно можно как-то, но я не знаю как). Есть еще вариант каждый раз лезть в рефлексию при непосредственно вызове метода и находить перегрузку, но это уже будет неоправданная трата проц. времени ради каких-то перегрузок.
- Невозможность в мульти-биндингах делать различное поведение конвертера в зависимости от числа переданных параметров. То есть, если функция конвертации определена для 3 параметров, то количество мульти-биндингов должно быть именно таким, в стандартном конвертере же можно сделать вариативное количество.
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Windows.Data;
using System.Windows.Markup;
using System.Xaml;
namespace Converters
{
public class ConvertFuncExtension : MarkupExtension
{
public ConvertFuncExtension()
{
}
public ConvertFuncExtension(string functionsExpression)
{
FunctionsExpression = functionsExpression;
}
public string FunctionsExpression { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
object rootObject = (serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider).RootObject;
MethodInfo convertMethod = null;
MethodInfo convertBackMethod = null;
ParseFunctionsExpression(out var convertType, out var convertMethodName, out var convertBackType, out var convertBackMethodName);
if (convertMethodName != null) {
var type = convertType ?? rootObject.GetType();
var flags = convertType != null ?
BindingFlags.Public | BindingFlags.Static :
BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
if ((convertMethod = type.GetMethod(convertMethodName, flags)) == null)
throw new ArgumentException($"Specified convert method {convertMethodName} not found on type {type.FullName}");
}
if (convertBackMethodName != null) {
var type = convertBackType ?? rootObject.GetType();
var flags = convertBackType != null ?
BindingFlags.Public | BindingFlags.Static :
BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
if ((convertBackMethod = type.GetMethod(convertBackMethodName, flags)) == null)
throw new ArgumentException($"Specified convert method {convertBackMethodName} not found on type {type.FullName}");
}
return new Converter(rootObject, convertMethod, convertBackMethod);
}
void ParseFunctionsExpression(out Type convertType, out string convertMethodName, out Type convertBackType, out string convertBackMethodName)
{
if (!ParseFunctionsExpressionWithRegex(out string commonConvertTypeName, out string fullConvertMethodName, out string fullConvertBackMethodName))
throw new ArgumentException("Error parsing functions expression");
Lazy<Type[]> allTypes = new Lazy<Type[]>(GetAllTypes);
Type commonConvertType = null;
if (commonConvertTypeName != null) {
commonConvertType = FindType(allTypes.Value, commonConvertTypeName);
if (commonConvertType == null)
throw new ArgumentException($"Error parsing functions expression: type {commonConvertTypeName} not found");
}
convertType = commonConvertType;
convertBackType = commonConvertType;
if (fullConvertMethodName != null)
ParseFullMethodName(allTypes, fullConvertMethodName, ref convertType, out convertMethodName);
else {
convertMethodName = null;
convertBackMethodName = null;
}
if (fullConvertBackMethodName != null)
ParseFullMethodName(allTypes, fullConvertBackMethodName, ref convertBackType, out convertBackMethodName);
else
convertBackMethodName = null;
}
bool ParseFunctionsExpressionWithRegex(out string commonConvertTypeName, out string fullConvertMethodName, out string fullConvertBackMethodName)
{
if (FunctionsExpression == null) {
commonConvertTypeName = null;
fullConvertMethodName = null;
fullConvertBackMethodName = null;
return true;
}
var match = _functionsExpressionRegex.Match(FunctionsExpression.Trim());
if (!match.Success) {
commonConvertTypeName = null;
fullConvertMethodName = null;
fullConvertBackMethodName = null;
return false;
}
commonConvertTypeName = match.Groups[1].Value;
if (commonConvertTypeName == "")
commonConvertTypeName = null;
fullConvertMethodName = match.Groups[2].Value.Trim();
if (fullConvertMethodName == "")
fullConvertMethodName = null;
fullConvertBackMethodName = match.Groups[3].Value.Trim();
if (fullConvertBackMethodName == "")
fullConvertBackMethodName = null;
return true;
}
static void ParseFullMethodName(Lazy<Type[]> allTypes, string fullMethodName, ref Type type, out string methodName)
{
var delimiterPos = fullMethodName.LastIndexOf('.');
if (delimiterPos == -1) {
methodName = fullMethodName;
return;
}
methodName = fullMethodName.Substring(delimiterPos + 1, fullMethodName.Length - (delimiterPos + 1));
var typeName = fullMethodName.Substring(0, delimiterPos);
var foundType = FindType(allTypes.Value, typeName);
type = foundType ?? throw new ArgumentException($"Error parsing functions expression: type {typeName} not found");
}
static Type FindType(Type[] types, string fullName)
=> types.FirstOrDefault(t => t.FullName.Equals(fullName));
static Type[] GetAllTypes()
=> AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).ToArray();
readonly Regex _functionsExpressionRegex = new Regex(
@"^(?:([^ ,]+) )?([^,]+)(?:,([^,]+))?(?:[\s\S]*)$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
class Converter : IValueConverter, IMultiValueConverter
{
public Converter(object rootObject, MethodInfo convertMethod, MethodInfo convertBackMethod)
{
_rootObject = rootObject;
_convertMethod = convertMethod;
_convertBackMethod = convertBackMethod;
_convertMethodParametersCount = _convertMethod != null ? _convertMethod.GetParameters().Length : 0;
_convertBackMethodParametersCount = _convertBackMethod != null ? _convertBackMethod.GetParameters().Length : 0;
}
#region IValueConverter
object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (_convertMethod == null)
return value;
if (_convertMethodParametersCount == 1)
return _convertMethod.Invoke(_rootObject, new[] { value });
else if (_convertMethodParametersCount == 2)
return _convertMethod.Invoke(_rootObject, new[] { value, parameter });
else
throw new InvalidOperationException("Method has invalid parameters");
}
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (_convertBackMethod == null)
return value;
if (_convertBackMethodParametersCount == 1)
return _convertBackMethod.Invoke(_rootObject, new[] { value });
else if (_convertBackMethodParametersCount == 2)
return _convertBackMethod.Invoke(_rootObject, new[] { value, parameter });
else
throw new InvalidOperationException("Method has invalid parameters");
}
#endregion
#region IMultiValueConverter
object IMultiValueConverter.Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (_convertMethod == null)
throw new ArgumentException("Convert function is not defined");
if (_convertMethodParametersCount == values.Length)
return _convertMethod.Invoke(_rootObject, values);
else if (_convertMethodParametersCount == values.Length + 1)
return _convertMethod.Invoke(_rootObject, ConcatParameters(values, parameter));
else
throw new InvalidOperationException("Method has invalid parameters");
}
object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
if (_convertBackMethod == null)
throw new ArgumentException("ConvertBack function is not defined");
object converted;
if (_convertBackMethodParametersCount == 1)
converted = _convertBackMethod.Invoke(_rootObject, new[] { value });
else if (_convertBackMethodParametersCount == 2)
converted = _convertBackMethod.Invoke(_rootObject, new[] { value, parameter });
else
throw new InvalidOperationException("Method has invalid parameters");
if (converted is object[] convertedAsArray)
return convertedAsArray;
// ToDo: Convert to object[] from Tuple<> and System.ValueTuple
return null;
}
static object[] ConcatParameters(object[] parameters, object converterParameter)
{
object[] result = new object[parameters.Length + 1];
parameters.CopyTo(result, 0);
result[parameters.Length] = converterParameter;
return result;
}
#endregion
object _rootObject;
MethodInfo _convertMethod;
MethodInfo _convertBackMethod;
int _convertMethodParametersCount;
int _convertBackMethodParametersCount;
}
}
}
Спасибо за внимание!
Комментарии (59)
dmitry_dvm
17.04.2017 11:25+1Что-то итоговая простыня не особо проще пачки, пусть даже и одноразовых, конвертеров. Как все это дружит с x:Bind?
impwx
17.04.2017 12:18Согласен, встроенный DSL для описания связей, обход всех типов в домене с помощью
Reflection
— по-моему, это стрельба по воробьям даже не из пушки, а из BFG9000 с орбиты.
Больше всего меня смущает использование строк для описания имени метода. Например, R# умеет статически проверять синтаксис биндингов и предупредит, если в имени конвертера опечатка. Здесь же мы об этом узнаем только в рантайме.
ultimabear
17.04.2017 20:53Ну обход всех типов происходит при вычислении значения конвертера, что происходит не так уж часто, в конвертере уже указатели лежат, пусть даже не самые быстрые. Ну возможно если динамически xaml грузить туда-сюда, то может и будет тормозить.
novar
17.04.2017 11:29для MVVM вообще не рекомендуется использовать code-behind от View, мне кажется конвертеры должны быть во ViewModel
msin
17.04.2017 13:49Согласен, писать конверторы очень неинтересно и утомительно.
Но я для себя давно решил эту проблему выбором правильного MVVM Framework, посмотрите тут:
https://documentation.devexpress.com/#WPF/CustomDocument115770
Shaddi
17.04.2017 13:49Если цель только уменьшить количество кода, можно делать так:
Базовый классpublic abstract class ConverterBase<T> : MarkupExtension, IValueConverter where T: class, new() { private static T instance; static ConverterBase() { ConverterBase<T>.instance = Activator.CreateInstance<T>(); } protected ConverterBase() { } public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture); public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } public override object ProvideValue(IServiceProvider serviceProvider) { if (ConverterBase<T>.instance == null) { ConverterBase<T>.instance = Activator.CreateInstance<T>(); } return ConverterBase<T>.instance; } public static T Instance { get { return ConverterBase<T>.instance; } } }
AgentFire
17.04.2017 13:49Есть один пакет, имхо, как раз для удобства создания конвертеров, там всего 2 женерик абстрактных класса, но с ними гораздо удобнее, чем IValueConverter каждый раз реализовывать: https://www.nuget.org/packages/AgentFire.Wpf.ValueConverters/
byme
17.04.2017 13:49Есть вот такая штука. Там конвекторы можно писать прямо во View(пример смотреть в разделе Binding Converters).
ultimabear
17.04.2017 19:34Вы ее использовали по-серьезному? Интересует насколько оно рабочее для больших проектов.
byme
17.04.2017 19:50На данный момент у меня есть только pet-project, где я ее использую, пока все устраивает. Багов, которые реально мешают работать, пока не обнаружил.
crea7or
17.04.2017 14:16Так теперь запоминать как работает ещё один класс — зачем? Сущностей стало ещё больше же, а как писал Уильям Оккама — не плоди сущности без необходимости.
Конвертеры же либо качуют из проекта в проект, либо пишутся какие-то специфические. В первом случае они вообще никогда не редактируются. А во втором… Зачем их тоже редактировать-то часто?
Vvintage
17.04.2017 16:19Небольшой офф-топ:
А почему используется слово «биндинг»?
Я всегда читал на английский манер — «байндинг».
Да и на dictionary.com указано так жеultimabear
17.04.2017 16:20Потому что по русски я его именно так и произношу. По английски по другому произношу.
Bonart
18.04.2017 11:44А потому, что "произносить на английский манер" — реализация негодной цели негодными средствами.
Общаетесь на русском — говорите по-русски, на английском — по-английски.
Пародия на английское произношение в русском — типичный антипаттерн, носителям русккого неудобно, носителям английского вообще фиолетово.Vvintage
18.04.2017 12:22На мой взгляд, если используется термин из английского языка, то и произносить его следует так, как носители этого языка.
Другое дело, когда термин перешел в профессиональный жаргон. Тогда, конечно, source code становится сорцами, а view — вьюхой.
Думаю тут именно второй вариант.Bonart
18.04.2017 12:42На мой взгляд, если используется термин из английского языка, то и произносить его следует так, как носители этого языка.
Такой взгляд — антипаттерн. Правильное произношение носителям другого языка скорее вредно, чем бесполезно, и в большинстве случаев недоступно. Заимствование иностранных слов по построению включает в себя произношение, характерное для родного.
Внесение в русский пародии на инглиш приведет только к затруднениям в общении с носителями русского.
DjoNIK
18.04.2017 12:52Общаетесь на русском — говорите по-русски, на английском — по-английски.
Следуя этой логике нужно сказать не биндинг, а привязка.Bonart
18.04.2017 13:01А вместо "клизма" — "задослаб"
Заимствование обычно полезно, когда позволяет сократить наименование или сузить смысл понятия.
Использование слова "биндинг" может быть оправдано в контекте разговора о WPF так как подразумевает не привязку "вообще", а конкретную ее реализацию. В результате заимствованное слово получается точнее, чем оба оригинальных ("привязка" и "binding")DjoNIK
18.04.2017 13:27То есть вместо общепринятого в одном или в другом языке, давайте-ка придумаем свое, верно я понял мысль? Биндинг — изначально неправильное прочтения заимствования. Никакой оправданности или точности в нем нет (ну или я так слеп, что не могу усмотреть).
HellMaster_HaiL
18.04.2017 16:26Я остановился в итоге на использовании стандартных конвертов. Правда использую упрощенную версию
генерик классаnamespace Converters { public abstract class BaseConverter : IValueConverter { protected abstract object Convert(object value); protected virtual object ConvertBack(object value) { return value; } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return Convert(value); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return ConvertBack(value); } } public abstract class BaseConverter<T> : BaseConverter { protected abstract object Convert(T value); protected override object Convert(object value) { return Convert(value is T ? (T)value : default(T)); } } public abstract class BaseConverter<T1, T2> : BaseConverter { protected abstract T2 Convert(T1 value); protected override object Convert(object value) { return Convert(value is T1 ? (T1)value : default(T1)); } } }
MonkAlex
В MVVM можно сделать всё проще — заставить VM предоставлять данные в нужном типе и всё.
Никаких конвертеров вам больше не надо.
impwx
Это не очень практичный вариант. Допустим, у вас есть форма с единственной кнопкой — нажимаем на нее, запускается длительное действие. В это время кнопка задизаблена, а на форме появляется текст «Идет обработка, пожалуйста подождите...». В вашем случае придется делать два свойства — одно
bool IsButtonDisabled
, другоеVisibility TextVisibility
и не забывать их оба обновлять. На примерах из реального мира таких свойств будет еще больше.DjoNIK
Ну и плюс Visibility в VM тоже не здорово.
MonkAlex
Почему? Признак видимости, напрямую биндящийся к вьюшке — по моему вполне рабочий вариант. Можно bool с конвертером, правда смысла в этом не вижу.
DjoNIK
А если разнести V, VM и M по разным проектам, то нужно прокидывать в VM сборки предназначенные только для UI.
MonkAlex
А оно у вас хоть раз так работало? Обычно получается так, что если хочется поменять вьюшку, то нужны изменения в её модели как минимум. Если разнести, да, выглядит криво. Только разнесенное оно и работать хз как должно.
Спорно, короче говоря.
ICELedyanoj
Использовать специфические WPF-типы в моделях? Да чё уж там — пишите сразу в коде формы, зачем вам модели в этом случае.
Сам смысл моделей в отделении логики от интерфейса, и использовать там UI-типы — это очень, очень грязно.
MonkAlex
Ещё раз, это не модели, это вью-модели. Их цель — описать логику поведения вьюшки.
Захотели добавить ещё одно условие к видимости контрола — дописали строчку кода, в разметку лезть не надо.
Bonart
Логику поведения (команда X не разрешена для выполнения, если выполняется в данный момент), а не отображения (кнопка для вызова команды X должна быть невидимой).
Специфическим для View типам во ViewModel места нет по построению.
ad1Dima
Вы говорите о разных вещах.
Да, вьюмодели описывают состояние View. Но от View они не должны зависить, в том числе от перечесления System.Windows.Visability. Сегодня вы используете в качестве View WPF, завтра UWP, где это перечисление Windows.UI.Xaml.Visibility, а послезавтра решите подключить Xamarin, где вообще нет аналога для этого перечисления.
MonkAlex
Звучит хорошо. Дайте только посмотреть хоть один пример, где реально одна ВМ на разные платформы?
ad1Dima
простите, но комерческий код с прошлой работы не буду показывать. Но мы писали кросс-платформенные MVVM приложения с общими VM и нативным UI (XAML/Xib/axml)
Но кроме Xamarin есть еще и разница между Winphone Silverlight и WinRt/UWP, для них во времена win8 как раз и придумали SharedProject, чтоб вьюмодели там лежали.
DjoNIK
Был у меня небольшой солюшн под WP 7.5 с 3+ проектами. M зависела только от утилитарного проекта и DAL; VM от M + от утилитарного; V, соответственно, от VM (без M, то есть V не знал о M вообще). При переходе 7.x->8.x->UWP мне по большому счету приходилось менять только V.
Да, мне очень помог Caliburn.Micro, позволивший не пробрасывать из V во VM *EventArg и не городить лишних зависимостей. А там где нужно было завязаться на специфические API — все решалось через DI.
Годится такой пример?
MonkAlex
Ага, спасибо за ответы. У меня ВМ обычно получаются разные, поэтому я не заморачивался и слабо верилось, что кто-то реально так работает.
Bonart
Это совсем не рабочий вариант.
ViewModel не должна ничего знать о визуализации, ее ответственность — давать необходимую информацию и доступ к командам.
MonkAlex
Почему нет? Хочу и делаю, не нарушаю никакой логики и всё работает отлично.
Чем принципиально биндинг текста отличается от биндинга видимости то?
Почему текст я могу менять в любой момент, а видимость надо конвертировать?
Не надо себя ограничивать по каким то религиозным причинам.
ad1Dima
DjoNIK
Серьезное заявление… проверять я его, конечно, не буду ©
novar
Для таких и подобных случаев делаются отдельные переиспользуемые компоненты типа «Кнопка со статусом». Например, для случая множества взаимосвязанных кнопок я создал "Инфраструктура команд для вызова пользователем действий в шаблоне MVVM".
MonkAlex
Да, свойств будет больше. Но каждое из них будет отвечать именно на то, что нужно — видимость текста и доступность кнопки. И если вам нужно этот признак изменить — вы его просто меняете. В случае с конвертерами вы это технически сделать не можете.
DjoNIK
Больше свойств — больше связей, все их нужно синхронизировать и держать в голове при разработке. Конвертеры имеют меньше зависимостей.
В любом случае нужен взвешенный подход. Нельзя однозначно отказаться от конвертеров, ровно как и от подхода создания дополнительных свойств. Наверное, только с опытом придет понимания, как лучше поступить в том или ином случае. А универсальных и строго формализованных правил на этот счет сформулировать трудно.
impwx
Свойства в VM обычно отображают состояния приложения, и их тут всего два — приложение либо совершает операцию, либо нет. Как именно отобразить процесс совершения операции — это уже детали реализацию View, при изменении которых VM может вообще не затрагиваться.
Чем больше гранулярность, тем сложнее поддерживать приложение. Когда новый программист взглянет на код и увидит отдельно свойства
TextVisibility
иIsButtonDisabled
, ему будет абсолютно неочевидно, что их всегда нужно использовать вместе. Так в проект могут проникнуть трудноуловимые логические баги.MonkAlex
Захотите вы добавить текст и без кнопки — существующий признак TextVisibility уже есть и работает. То, что у вас текст заменяет собой кнопку — логика, вполне достойная быть описанной в VM.
Bonart
Не должно быть в интерфейсе ViewModel понятий "видимость текста" и "доступность кнопки"
ad1Dima
Почему?
Bonart
По определению паттерна MVVM.
MonkAlex
А если я, как вью-модель, говорю что текст неактуален в такой то момент времени и сообщаю об этом посредством признака «видимость текста», то почему простите этот признак нельзя держать во вью-модели?
Bonart
Можно в общем-то все, включая суицид.
Последствия в виде роста стоимости доработок чуть быстрее экспоненты — за свой счет.
ad1Dima
Другими словами, за состояние UI. включать или не включать отображение конкретного текста в бизнесс-логику решается к каждом конкретном случае. Не вижу причин принципиально этого не делать. Особенно если состояние кнопки и надписи независимы.
То будет ли текст отображаться, и является ли кнопка активной это не то, как рисуется View. View каждое из этих состояний может трактовать десятком различный способов, определять анимацию переходов из одного состояния в другое. Но выносить логику показа надписи в конвертер — это головная боль в будущем.
DjoNIK
Возражу — доступность кнопки может быть обусловлена бизнес-правилами, а не UI. Если рассуждать не в контексте Кнопка, Доступность Кнопки, а в категориях Команда (отправка данных на сервер), Доступность Команды (не все поля заполнены), то вполне разумно в VM добавить такую функциональность, которую, к тому же, легко проверить тестами.
Bonart
Бизнес-правилами обусловлена доступность не кнопки, а команды.
А вот доступность команды в доступность кнопки (или пункта меню, или шотката, или еще чего-нибудь) превращает View
Deosis
Минус данного подхода: модель начинает подстраиваться под представление. Каждый раз когда нам понадобится изменить представление данных, придется изменять модель.
Рефлексия работает очень медленно, но в данном случае она вызывается редко.ПС.
ППС. Я буду обновлять комментарии
MonkAlex
Не модель, а ВМ. И это, извините, её предназначение — модель вьюшки как никак.
ultimabear
А если эта же VM используется еще и на андроиде, где там взять тип System.Windows.Visibility (Пример для конвертера из bool в Visibility)?
MonkAlex
У вас реально есть такой проект? Не сталкивался с ВМ, которые бы переиспользовались реально. Ссылкой по возможности поделитесь, посмотреть.