Около года уже работаю с 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. MonkAlex
    17.04.2017 11:13
    +1

    В MVVM можно сделать всё проще — заставить VM предоставлять данные в нужном типе и всё.
    Никаких конвертеров вам больше не надо.


    1. impwx
      17.04.2017 11:24
      +1

      Это не очень практичный вариант. Допустим, у вас есть форма с единственной кнопкой — нажимаем на нее, запускается длительное действие. В это время кнопка задизаблена, а на форме появляется текст «Идет обработка, пожалуйста подождите...». В вашем случае придется делать два свойства — одно bool IsButtonDisabled, другое Visibility TextVisibility и не забывать их оба обновлять. На примерах из реального мира таких свойств будет еще больше.


      1. DjoNIK
        17.04.2017 11:34
        +2

        Ну и плюс Visibility в VM тоже не здорово.


        1. MonkAlex
          17.04.2017 11:40

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


          1. DjoNIK
            17.04.2017 11:58

            А если разнести V, VM и M по разным проектам, то нужно прокидывать в VM сборки предназначенные только для UI.


            1. MonkAlex
              17.04.2017 12:14

              А оно у вас хоть раз так работало? Обычно получается так, что если хочется поменять вьюшку, то нужны изменения в её модели как минимум. Если разнести, да, выглядит криво. Только разнесенное оно и работать хз как должно.

              Спорно, короче говоря.


          1. ICELedyanoj
            18.04.2017 10:41

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


            1. MonkAlex
              18.04.2017 11:28
              -2

              Ещё раз, это не модели, это вью-модели. Их цель — описать логику поведения вьюшки.

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


              1. Bonart
                18.04.2017 11:46
                +1

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


              1. ad1Dima
                18.04.2017 11:53
                +1

                Вы говорите о разных вещах.
                Да, вьюмодели описывают состояние View. Но от View они не должны зависить, в том числе от перечесления System.Windows.Visability. Сегодня вы используете в качестве View WPF, завтра UWP, где это перечисление Windows.UI.Xaml.Visibility, а послезавтра решите подключить Xamarin, где вообще нет аналога для этого перечисления.


                1. MonkAlex
                  18.04.2017 12:09
                  +1

                  Звучит хорошо. Дайте только посмотреть хоть один пример, где реально одна ВМ на разные платформы?


                  1. ad1Dima
                    18.04.2017 12:37
                    +2

                    простите, но комерческий код с прошлой работы не буду показывать. Но мы писали кросс-платформенные MVVM приложения с общими VM и нативным UI (XAML/Xib/axml)

                    Но кроме Xamarin есть еще и разница между Winphone Silverlight и WinRt/UWP, для них во времена win8 как раз и придумали SharedProject, чтоб вьюмодели там лежали.


                  1. DjoNIK
                    18.04.2017 12:42
                    +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.

                    Годится такой пример?


                  1. MonkAlex
                    18.04.2017 14:06

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


          1. Bonart
            18.04.2017 11:37
            +1

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


            1. MonkAlex
              18.04.2017 12:11
              -1

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

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

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

              Не надо себя ограничивать по каким то религиозным причинам.


              1. ad1Dima
                18.04.2017 12:39
                +1

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


              1. DjoNIK
                18.04.2017 12:44
                +1

                Хочу и делаю
                и всё работает отлично

                Серьезное заявление… проверять я его, конечно, не буду ©


      1. novar
        17.04.2017 11:35

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


      1. MonkAlex
        17.04.2017 11:39

        Да, свойств будет больше. Но каждое из них будет отвечать именно на то, что нужно — видимость текста и доступность кнопки. И если вам нужно этот признак изменить — вы его просто меняете. В случае с конвертерами вы это технически сделать не можете.


        1. DjoNIK
          17.04.2017 12:03

          Больше свойств — больше связей, все их нужно синхронизировать и держать в голове при разработке. Конвертеры имеют меньше зависимостей.

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


        1. impwx
          17.04.2017 12:04

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

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


          1. MonkAlex
            17.04.2017 12:16

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


        1. Bonart
          18.04.2017 11:38

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


          1. ad1Dima
            18.04.2017 11:54

            Почему?


            1. Bonart
              18.04.2017 12:07

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


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


              1. MonkAlex
                18.04.2017 12:12

                А если я, как вью-модель, говорю что текст неактуален в такой то момент времени и сообщаю об этом посредством признака «видимость текста», то почему простите этот признак нельзя держать во вью-модели?


                1. Bonart
                  18.04.2017 12:30

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


              1. ad1Dima
                18.04.2017 12:44
                +1

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

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

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


          1. DjoNIK
            18.04.2017 12:48

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


            1. Bonart
              18.04.2017 14:22

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


    1. Deosis
      17.04.2017 12:19

      Минус данного подхода: модель начинает подстраиваться под представление. Каждый раз когда нам понадобится изменить представление данных, придется изменять модель.
      ПС.

      вообще я не уверен что MethodInfo.Invoke() работает так же быстро, как вызов метода напрямую
      Рефлексия работает очень медленно, но в данном случае она вызывается редко.
      ППС. Я буду обновлять комментарии


      1. MonkAlex
        17.04.2017 12:20
        -1

        Не модель, а ВМ. И это, извините, её предназначение — модель вьюшки как никак.


    1. ultimabear
      17.04.2017 13:11

      А если эта же VM используется еще и на андроиде, где там взять тип System.Windows.Visibility (Пример для конвертера из bool в Visibility)?


      1. MonkAlex
        17.04.2017 13:12
        -1

        У вас реально есть такой проект? Не сталкивался с ВМ, которые бы переиспользовались реально. Ссылкой по возможности поделитесь, посмотреть.


  1. dmitry_dvm
    17.04.2017 11:25
    +1

    Что-то итоговая простыня не особо проще пачки, пусть даже и одноразовых, конвертеров. Как все это дружит с x:Bind?


    1. impwx
      17.04.2017 12:18

      Согласен, встроенный DSL для описания связей, обход всех типов в домене с помощью Reflection — по-моему, это стрельба по воробьям даже не из пушки, а из BFG9000 с орбиты.

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


      1. ad1Dima
        17.04.2017 18:42

        Студия тоже это умеет )


      1. ultimabear
        17.04.2017 20:53

        Ну обход всех типов происходит при вычислении значения конвертера, что происходит не так уж часто, в конвертере уже указатели лежат, пусть даже не самые быстрые. Ну возможно если динамически xaml грузить туда-сюда, то может и будет тормозить.


  1. novar
    17.04.2017 11:29

    для MVVM вообще не рекомендуется использовать code-behind от View, мне кажется конвертеры должны быть во ViewModel


  1. msin
    17.04.2017 13:49

    Согласен, писать конверторы очень неинтересно и утомительно.
    Но я для себя давно решил эту проблему выбором правильного MVVM Framework, посмотрите тут:
    https://documentation.devexpress.com/#WPF/CustomDocument115770


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


  1. AgentFire
    17.04.2017 13:49

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


  1. byme
    17.04.2017 13:49

    Есть вот такая штука. Там конвекторы можно писать прямо во View(пример смотреть в разделе Binding Converters).


    1. ultimabear
      17.04.2017 19:34

      Вы ее использовали по-серьезному? Интересует насколько оно рабочее для больших проектов.


      1. byme
        17.04.2017 19:50

        На данный момент у меня есть только pet-project, где я ее использую, пока все устраивает. Багов, которые реально мешают работать, пока не обнаружил.


  1. crea7or
    17.04.2017 14:16

    Так теперь запоминать как работает ещё один класс — зачем? Сущностей стало ещё больше же, а как писал Уильям Оккама — не плоди сущности без необходимости.

    Конвертеры же либо качуют из проекта в проект, либо пишутся какие-то специфические. В первом случае они вообще никогда не редактируются. А во втором… Зачем их тоже редактировать-то часто?


  1. Vvintage
    17.04.2017 16:19

    Небольшой офф-топ:
    А почему используется слово «биндинг»?
    Я всегда читал на английский манер — «байндинг».
    Да и на dictionary.com указано так же


    1. ultimabear
      17.04.2017 16:20

      Потому что по русски я его именно так и произношу. По английски по другому произношу.


    1. AgentFire
      17.04.2017 16:37

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


    1. Bonart
      18.04.2017 11:44

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


      1. Vvintage
        18.04.2017 12:22

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


        1. Bonart
          18.04.2017 12:42

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

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


          1. dmitry_dvm
            18.04.2017 18:00

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


            1. Bonart
              18.04.2017 18:20

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


      1. DjoNIK
        18.04.2017 12:52

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


        1. Bonart
          18.04.2017 13:01

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


          1. DjoNIK
            18.04.2017 13:27

            То есть вместо общепринятого в одном или в другом языке, давайте-ка придумаем свое, верно я понял мысль? Биндинг — изначально неправильное прочтения заимствования. Никакой оправданности или точности в нем нет (ну или я так слеп, что не могу усмотреть).


  1. 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));
            }
        }
    }