Упрощаем конвертеры для WPF

Около года уже работаю с WPF и некоторые вещи в нем откровенно выбешивают. Одна из таких вещей — конвертеры. Ради каждого чиха объявлять реализацию сомнительно выглядящего интерфейса где-то в недрах проекта, а потом искать его через Ctrl+F по названию, когда он вдруг понадобится. В мульти-конвертерах так вообще сам черт запутается.

Ситуацию усугубляет 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 без реализации.

Идей по упрощению этого у меня несколько:

  1. Конвертеры в виде кусков c# кода прямо в xaml для простых вычислений;
  2. Конвертеры в виде ссылок на методы в code-behind, для случаев с очень конкретными/частными случаями конвертации, то есть тогда, когда нет смысла эту конвертацию где-либо еще переиспользовать;
  3. Конвертеры в виде того-же самого, что в стандартной реализации, но чтобы это не выглядело так стремно, чтобы каждый раз при написании нового конвертера не приходилось лезть в гугл и искать пример реализации конвертера.

Сразу обломаю тех из вас, кто думает, что я буду рассказывать как реализовать пункт 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>

Минусы моей реализации, которые я нашел:

  1. В момент вызова метода идут всякие проверки, создание массивов для параметров, и вообще я не уверен что MethodInfo.Invoke() работает так же быстро, как вызов метода напрямую, однако я бы не сказал что это большой минус в условиях работы с WPF/MVVM.

  2. Нет возможности использовать перегрузки, так как на момент получения MethodInfo неизвестны типы значений, которые будут приходить, а значит нельзя получить нужную перегрузку метода в этот момент (Возможно можно как-то, но я не знаю как). Есть еще вариант каждый раз лезть в рефлексию при непосредственно вызове метода и находить перегрузку, но это уже будет неоправданная трата проц. времени ради каких-то перегрузок.

  3. Невозможность в мульти-биндингах делать различное поведение конвертера в зависимости от числа переданных параметров. То есть, если функция конвертации определена для 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
    +1
    В MVVM можно сделать всё проще — заставить VM предоставлять данные в нужном типе и всё.
    Никаких конвертеров вам больше не надо.
      +1
      Это не очень практичный вариант. Допустим, у вас есть форма с единственной кнопкой — нажимаем на нее, запускается длительное действие. В это время кнопка задизаблена, а на форме появляется текст «Идет обработка, пожалуйста подождите...». В вашем случае придется делать два свойства — одно bool IsButtonDisabled, другое Visibility TextVisibility и не забывать их оба обновлять. На примерах из реального мира таких свойств будет еще больше.
        +2
        Ну и плюс Visibility в VM тоже не здорово.
          0
          Почему? Признак видимости, напрямую биндящийся к вьюшке — по моему вполне рабочий вариант. Можно bool с конвертером, правда смысла в этом не вижу.
            0
            А если разнести V, VM и M по разным проектам, то нужно прокидывать в VM сборки предназначенные только для UI.
              0
              А оно у вас хоть раз так работало? Обычно получается так, что если хочется поменять вьюшку, то нужны изменения в её модели как минимум. Если разнести, да, выглядит криво. Только разнесенное оно и работать хз как должно.

              Спорно, короче говоря.
              0
              Использовать специфические WPF-типы в моделях? Да чё уж там — пишите сразу в коде формы, зачем вам модели в этом случае.
              Сам смысл моделей в отделении логики от интерфейса, и использовать там UI-типы — это очень, очень грязно.
                –2
                Ещё раз, это не модели, это вью-модели. Их цель — описать логику поведения вьюшки.

                Захотели добавить ещё одно условие к видимости контрола — дописали строчку кода, в разметку лезть не надо.
                  +1

                  Логику поведения (команда X не разрешена для выполнения, если выполняется в данный момент), а не отображения (кнопка для вызова команды X должна быть невидимой).
                  Специфическим для View типам во ViewModel места нет по построению.

                    +1
                    Вы говорите о разных вещах.
                    Да, вьюмодели описывают состояние View. Но от View они не должны зависить, в том числе от перечесления System.Windows.Visability. Сегодня вы используете в качестве View WPF, завтра UWP, где это перечисление Windows.UI.Xaml.Visibility, а послезавтра решите подключить Xamarin, где вообще нет аналога для этого перечисления.
                      +1
                      Звучит хорошо. Дайте только посмотреть хоть один пример, где реально одна ВМ на разные платформы?
                        +2
                        простите, но комерческий код с прошлой работы не буду показывать. Но мы писали кросс-платформенные MVVM приложения с общими VM и нативным UI (XAML/Xib/axml)

                        Но кроме Xamarin есть еще и разница между Winphone Silverlight и WinRt/UWP, для них во времена win8 как раз и придумали SharedProject, чтоб вьюмодели там лежали.
                          +1
                          Был у меня небольшой солюшн под 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.

                          Годится такой пример?
                            0
                            Ага, спасибо за ответы. У меня ВМ обычно получаются разные, поэтому я не заморачивался и слабо верилось, что кто-то реально так работает.
                      +1

                      Это совсем не рабочий вариант.
                      ViewModel не должна ничего знать о визуализации, ее ответственность — давать необходимую информацию и доступ к командам.

                        –1
                        Почему нет? Хочу и делаю, не нарушаю никакой логики и всё работает отлично.

                        Чем принципиально биндинг текста отличается от биндинга видимости то?

                        Почему текст я могу менять в любой момент, а видимость надо конвертировать?

                        Не надо себя ограничивать по каким то религиозным причинам.
                          +1
                          Чем принципиально биндинг текста отличается от биндинга видимости то?
                          объектом из слоя UI.
                            +1
                            Хочу и делаю
                            и всё работает отлично

                            Серьезное заявление… проверять я его, конечно, не буду ©
                      0
                      Для таких и подобных случаев делаются отдельные переиспользуемые компоненты типа «Кнопка со статусом». Например, для случая множества взаимосвязанных кнопок я создал "Инфраструктура команд для вызова пользователем действий в шаблоне MVVM".
                        0
                        Да, свойств будет больше. Но каждое из них будет отвечать именно на то, что нужно — видимость текста и доступность кнопки. И если вам нужно этот признак изменить — вы его просто меняете. В случае с конвертерами вы это технически сделать не можете.
                          0
                          Больше свойств — больше связей, все их нужно синхронизировать и держать в голове при разработке. Конвертеры имеют меньше зависимостей.

                          В любом случае нужен взвешенный подход. Нельзя однозначно отказаться от конвертеров, ровно как и от подхода создания дополнительных свойств. Наверное, только с опытом придет понимания, как лучше поступить в том или ином случае. А универсальных и строго формализованных правил на этот счет сформулировать трудно.
                            0
                            Свойства в VM обычно отображают состояния приложения, и их тут всего два — приложение либо совершает операцию, либо нет. Как именно отобразить процесс совершения операции — это уже детали реализацию View, при изменении которых VM может вообще не затрагиваться.

                            Чем больше гранулярность, тем сложнее поддерживать приложение. Когда новый программист взглянет на код и увидит отдельно свойства TextVisibility и IsButtonDisabled, ему будет абсолютно неочевидно, что их всегда нужно использовать вместе. Так в проект могут проникнуть трудноуловимые логические баги.
                              0
                              Захотите вы добавить текст и без кнопки — существующий признак TextVisibility уже есть и работает. То, что у вас текст заменяет собой кнопку — логика, вполне достойная быть описанной в VM.
                              0

                              Не должно быть в интерфейсе ViewModel понятий "видимость текста" и "доступность кнопки"

                                0
                                Почему?
                                  0

                                  По определению паттерна MVVM.


                                  1. View и никто другой отвечает за визуализацию
                                  2. ViewModel отвечает за поведение интерфейса пользователя (индикаторы, команды, поток UI), но не за то, как это будет отображаться.
                                  3. Model отвечает за API
                                    0
                                    А если я, как вью-модель, говорю что текст неактуален в такой то момент времени и сообщаю об этом посредством признака «видимость текста», то почему простите этот признак нельзя держать во вью-модели?
                                      0

                                      Можно в общем-то все, включая суицид.
                                      Последствия в виде роста стоимости доработок чуть быстрее экспоненты — за свой счет.

                                      +1
                                      ViewModel отвечает за поведение интерфейса пользователя (индикаторы, команды, поток UI), но не за то, как это будет отображаться.

                                      Другими словами, за состояние UI. включать или не включать отображение конкретного текста в бизнесс-логику решается к каждом конкретном случае. Не вижу причин принципиально этого не делать. Особенно если состояние кнопки и надписи независимы.

                                      То будет ли текст отображаться, и является ли кнопка активной это не то, как рисуется View. View каждое из этих состояний может трактовать десятком различный способов, определять анимацию переходов из одного состояния в другое. Но выносить логику показа надписи в конвертер — это головная боль в будущем.

                                    0
                                    видимость текста
                                    Соглашусь.
                                    доступность кнопки
                                    Возражу — доступность кнопки может быть обусловлена бизнес-правилами, а не UI. Если рассуждать не в контексте Кнопка, Доступность Кнопки, а в категориях Команда (отправка данных на сервер), Доступность Команды (не все поля заполнены), то вполне разумно в VM добавить такую функциональность, которую, к тому же, легко проверить тестами.
                                      0

                                      Бизнес-правилами обусловлена доступность не кнопки, а команды.
                                      А вот доступность команды в доступность кнопки (или пункта меню, или шотката, или еще чего-нибудь) превращает View

                                0
                                Минус данного подхода: модель начинает подстраиваться под представление. Каждый раз когда нам понадобится изменить представление данных, придется изменять модель.
                                ПС.
                                вообще я не уверен что MethodInfo.Invoke() работает так же быстро, как вызов метода напрямую
                                Рефлексия работает очень медленно, но в данном случае она вызывается редко.
                                ППС. Я буду обновлять комментарии
                                  –1
                                  Не модель, а ВМ. И это, извините, её предназначение — модель вьюшки как никак.
                                  0
                                  А если эта же VM используется еще и на андроиде, где там взять тип System.Windows.Visibility (Пример для конвертера из bool в Visibility)?
                                    –1
                                    У вас реально есть такой проект? Не сталкивался с ВМ, которые бы переиспользовались реально. Ссылкой по возможности поделитесь, посмотреть.
                                  +1
                                  Что-то итоговая простыня не особо проще пачки, пусть даже и одноразовых, конвертеров. Как все это дружит с x:Bind?
                                    0
                                    Согласен, встроенный DSL для описания связей, обход всех типов в домене с помощью Reflection — по-моему, это стрельба по воробьям даже не из пушки, а из BFG9000 с орбиты.

                                    Больше всего меня смущает использование строк для описания имени метода. Например, R# умеет статически проверять синтаксис биндингов и предупредит, если в имени конвертера опечатка. Здесь же мы об этом узнаем только в рантайме.

                                      0
                                      Студия тоже это умеет )
                                        0
                                        Ну обход всех типов происходит при вычислении значения конвертера, что происходит не так уж часто, в конвертере уже указатели лежат, пусть даже не самые быстрые. Ну возможно если динамически xaml грузить туда-сюда, то может и будет тормозить.
                                      0
                                      для MVVM вообще не рекомендуется использовать code-behind от View, мне кажется конвертеры должны быть во ViewModel
                                        0
                                        Согласен, писать конверторы очень неинтересно и утомительно.
                                        Но я для себя давно решил эту проблему выбором правильного MVVM Framework, посмотрите тут:
                                        https://documentation.devexpress.com/#WPF/CustomDocument115770
                                          0
                                          Если цель только уменьшить количество кода, можно делать так:
                                          Базовый класс
                                          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;
                                          		}
                                          	}
                                          }
                                          


                                          Пример использования
                                          internal class DelayToTimeSpanConverter : ConverterBase<DelayToTimeSpanConverter>
                                          {
                                          	public override object Convert(
                                          		object value, Type targetType, object parameter, CultureInfo culture)
                                          	{
                                          		if (ReferenceEquals(value, null))
                                          		{
                                          			return DependencyProperty.UnsetValue;
                                          		}
                                          
                                          		return TimeSpan.FromMilliseconds(System.Convert.ToDouble(value));
                                          	}
                                          }
                                          


                                          Разметка WPF
                                          <TextBlock Text="{Binding Path=Span, Converter={conv:DelayToTimeSpanConverter}, StringFormat=hh\\:mm\\:ss}" />
                                          


                                            0

                                            Есть один пакет, имхо, как раз для удобства создания конвертеров, там всего 2 женерик абстрактных класса, но с ними гораздо удобнее, чем IValueConverter каждый раз реализовывать: https://www.nuget.org/packages/AgentFire.Wpf.ValueConverters/

                                              0
                                              Есть вот такая штука. Там конвекторы можно писать прямо во View(пример смотреть в разделе Binding Converters).
                                                0
                                                Вы ее использовали по-серьезному? Интересует насколько оно рабочее для больших проектов.
                                                  0
                                                  На данный момент у меня есть только pet-project, где я ее использую, пока все устраивает. Багов, которые реально мешают работать, пока не обнаружил.
                                                0
                                                Так теперь запоминать как работает ещё один класс — зачем? Сущностей стало ещё больше же, а как писал Уильям Оккама — не плоди сущности без необходимости.

                                                Конвертеры же либо качуют из проекта в проект, либо пишутся какие-то специфические. В первом случае они вообще никогда не редактируются. А во втором… Зачем их тоже редактировать-то часто?
                                                  0
                                                  Небольшой офф-топ:
                                                  А почему используется слово «биндинг»?
                                                  Я всегда читал на английский манер — «байндинг».
                                                  Да и на dictionary.com указано так же
                                                    0
                                                    Потому что по русски я его именно так и произношу. По английски по другому произношу.
                                                      0

                                                      "биндинг" быстрее

                                                        0

                                                        А потому, что "произносить на английский манер" — реализация негодной цели негодными средствами.
                                                        Общаетесь на русском — говорите по-русски, на английском — по-английски.
                                                        Пародия на английское произношение в русском — типичный антипаттерн, носителям русккого неудобно, носителям английского вообще фиолетово.

                                                          0
                                                          На мой взгляд, если используется термин из английского языка, то и произносить его следует так, как носители этого языка.
                                                          Другое дело, когда термин перешел в профессиональный жаргон. Тогда, конечно, source code становится сорцами, а view — вьюхой.
                                                          Думаю тут именно второй вариант.
                                                            0
                                                            На мой взгляд, если используется термин из английского языка, то и произносить его следует так, как носители этого языка.

                                                            Такой взгляд — антипаттерн. Правильное произношение носителям другого языка скорее вредно, чем бесполезно, и в большинстве случаев недоступно. Заимствование иностранных слов по построению включает в себя произношение, характерное для родного.
                                                            Внесение в русский пародии на инглиш приведет только к затруднениям в общении с носителями русского.

                                                              0
                                                              Чем слово binding характернее для русского как биндинг, а не байндинг?
                                                                0

                                                                Не лингвист, но как-то не припомню русских слов с "-айнд-"

                                                            0
                                                            Общаетесь на русском — говорите по-русски, на английском — по-английски.
                                                            Следуя этой логике нужно сказать не биндинг, а привязка.
                                                              0

                                                              А вместо "клизма" — "задослаб"
                                                              Заимствование обычно полезно, когда позволяет сократить наименование или сузить смысл понятия.
                                                              Использование слова "биндинг" может быть оправдано в контекте разговора о WPF так как подразумевает не привязку "вообще", а конкретную ее реализацию. В результате заимствованное слово получается точнее, чем оба оригинальных ("привязка" и "binding")

                                                                0
                                                                То есть вместо общепринятого в одном или в другом языке, давайте-ка придумаем свое, верно я понял мысль? Биндинг — изначально неправильное прочтения заимствования. Никакой оправданности или точности в нем нет (ну или я так слеп, что не могу усмотреть).
                                                          0
                                                          Я остановился в итоге на использовании стандартных конвертов. Правда использую упрощенную версию
                                                          генерик класса
                                                          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));
                                                                  }
                                                              }
                                                          }
                                                          



                                                          Может и не лучшая реализация, но для моих 90% случаев она подходит.
                                                          Главное — это понимать, что конвертеры — это некоторые «хелперы» слоя вьюшки, и они должны быть максимально отделены от других слоев. Т.е. когда у меня возникает соблазн или необходимость конвертировать какой либо класс из вьюмодел или модел, то я сперва сильно задумаюсь. а нельзя ли это сделать каким либо другим архитектурным решением. Таким образом довольно быстро формируется набор «стандартных» конвертов, которые живут в своей библиотечке.

                                                          В качестве уменьшения кода, вдохновившись статьёй, быстро сообразил свой велосипед, который состоит из одной реализации интерфейса IValueConverter (мультиконверторы и мультибиндинг я стараюсь не использовать, но добавить будет не сложно). Если это уже кем-то реализовано, то прошу прощения.

                                                          Code-behind
                                                          namespace Converters
                                                          {
                                                              public delegate object ConvertHandler(object value, Type targetType, object parameter, CultureInfo culture);
                                                          
                                                              public class ValueConverter : IValueConverter
                                                              {
                                                                  public ConvertHandler ConvertHandler { get; set; }
                                                                  public ConvertHandler ConvertBackHandler { get; set; }
                                                          
                                                                  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
                                                                  {
                                                                      return ConvertHandler?.Invoke(value, targetType, parameter, culture) ?? value;
                                                                  }
                                                          
                                                                  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
                                                                  {
                                                                      return ConvertBackHandler?.Invoke(value, targetType, parameter, culture) ?? value;
                                                                  }
                                                              }
                                                          
                                                              public class ConvertersFactory
                                                              {
                                                                  public static string NullableValue { get; set; }
                                                          
                                                                  public static ConvertHandler IntToString => (value, type, parameter, culture) => value.ToString();
                                                                  public static ConvertHandler NullableIntToString => (value, type, parameter, culture) => value is int ? value.ToString() : NullableValue;
                                                                  
                                                              }
                                                          }
                                                          


                                                          XAML
                                                          <UserControl x:Class="ConverterControl"
                                                                       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                                                                       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                                                                       xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                                                                       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                                                                       xmlns:local="clr-namespace:Converters">
                                                          
                                                              <UserControl.Resources>
                                                                 <local:ValueConverter x:Key="IntToStringConverter"
                                                                                        ConvertHandler="{x:Static local:ConvertersFactory.NullableIntToString}" />
                                                              </UserControl.Resources>
                                                          
                                                              <Grid>
                                                                  <TextBlock Text="{Binding Converter={StaticResource IntToStringConverter}}"/>
                                                              </Grid>
                                                          </UserControl>
                                                          
                                                          



                                                          Может не очень красиво, но кода получается немного меньше, есть некая гибкость, возможность запихнуть все конвертеры в один класс, который вообще не зависит от wpf. Минус — статические классы, по-быстрому не смог сообразить, почему кзамл не хочет динамически биндить делегаты.
                                                          Но самое главное, для стороннего WPF-разработчика код выглядит более-менее стандартно.

                                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                          Самое читаемое