Как стать автором
Обновить

Комментарии 61

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

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

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

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

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

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

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

Согласен, для этого сущевстуют маппинги

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

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

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

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

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

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

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

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

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

Почему?

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

То есть вместо общепринятого в одном или в другом языке, давайте-ка придумаем свое, верно я понял мысль? Биндинг — изначально неправильное прочтения заимствования. Никакой оправданности или точности в нем нет (ну или я так слеп, что не могу усмотреть).
Я остановился в итоге на использовании стандартных конвертов. Правда использую упрощенную версию
генерик класса
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-разработчика код выглядит более-менее стандартно.
Похоже, для ваших целей может подойти встраиваемый конвертер (Inline Converter), который позволяет помещать лоику конвертирования в code-behaind на основе событийной модели.
Детали тут:
habr.com/ru/post/526450
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории