Я работаю над приложением WPF с представлениями, которые требуют многочисленных преобразований значений. Изначально моя философия (Вдохновленный частично этой оживленной дискуссии по XAML Disciples ) было то , что я должен сделать модель представления строго о поддержке данных требований зрения. Это означало, что любые преобразования значений, необходимые для преобразования данных в такие вещи, как видимости, кисти, размеры и т. Д., Будут обрабатываться с помощью преобразователей значений и многозначных преобразователей. Концептуально это казалось довольно элегантным. Модель представления и представление имели бы различную цель и были бы хорошо отделены. Будет проведена четкая грань между «данными» и «внешним видом».
Что ж, после того, как я попробовал эту стратегию "попытаться в старом колледже", у меня возникли сомнения, хочу ли я продолжать развиваться таким образом. Я на самом деле настоятельно рекомендую сбросить преобразователи значений и возложить ответственность за (почти) все преобразования значений прямо на руки модели представления.
Реальность использования преобразователей значений просто не соответствует очевидной ценности четко разделенных задач. Моя самая большая проблема с конвертерами значений заключается в том, что их утомительно использовать. Вы должны создать новый класс, реализовать IValueConverter
или преобразовать IMultiValueConverter
значение или значения object
в правильный тип, проверить DependencyProperty.Unset
(по крайней мере, для многозначных преобразователей), написать логику преобразования, зарегистрировать преобразователь в словаре ресурсов [см. Обновление ниже ] и, наконец, подключите конвертер, используя довольно многословный XAML (который требует использования магических строк как для привязки (ей), так и для имени конвертера[см. обновление ниже]). Процесс отладки тоже не пикник, поскольку сообщения об ошибках часто бывают загадочными, особенно в режиме разработки Visual Studio / Expression Blend.
Нельзя сказать, что альтернатива - сделать модель представления ответственной за все преобразования значений - это улучшение. Это вполне может быть связано с тем, что трава с другой стороны зеленее. Помимо потери элегантного разделения интересов, вы должны написать кучу производных свойств и убедиться, что вы добросовестно вызываете их RaisePropertyChanged(() => DerivedProperty)
при настройке базовых свойств, что может оказаться неприятной проблемой обслуживания.
Ниже приведен первоначальный список плюсов и минусов, которые позволяют моделям представления обрабатывать логику преобразования и покончить с преобразователями значений:
- Плюсы:
- Меньше общего количества привязок, так как устранены мульти-конвертеры
- Меньше волшебных строк (пути привязки
+ имена ресурсов конвертера) Больше не нужно регистрировать каждый конвертер (плюс ведение этого списка)- Меньше работы по написанию каждого конвертера (не требуется реализации интерфейсов или приведения)
- Может легко вводить зависимости, чтобы помочь с преобразованиями (например, таблицы цветов)
- Разметка XAML менее многословна и ее легче читать
- Повторное использование конвертера все еще возможно (хотя требуется некоторое планирование)
- Никаких загадочных проблем с DependencyProperty.Unset (проблема, которую я заметил с многозначными преобразователями)
* Зачеркивания указывают на преимущества, которые исчезают, если вы используете расширения разметки (см. Обновление ниже)
- Минусы:
- Более сильная связь между моделью представления и представлением (например, свойства должны иметь дело с такими понятиями, как видимость и кисти)
- Больше общих свойств, позволяющих прямое сопоставление для каждой привязки в представлении
(см. обновление 2 ниже)RaisePropertyChanged
должен вызываться для каждого производного свойства- Необходимо по-прежнему полагаться на преобразователи, если преобразование основано на свойстве элемента пользовательского интерфейса
Итак, как вы, вероятно, можете сказать, у меня изжога по этому вопросу. Я очень не решаюсь идти по пути рефакторинга, только чтобы понять, что процесс кодирования столь же неэффективен и утомителен, использую ли я преобразователи значений или выставляю многочисленные свойства преобразования значений в моей модели представления.
Я пропускаю какие-либо плюсы / минусы? Для тех, кто попробовал оба способа преобразования стоимости, какой из них, по вашему мнению, работал лучше для вас и почему? Есть ли другие альтернативы? (Ученики упомянули кое-что о поставщиках дескрипторов типов, но я не мог понять, о чем они говорили. Любое понимание этого будет оценено.)
Обновить
Сегодня я обнаружил, что можно использовать то, что называется «расширением разметки», чтобы исключить необходимость регистрации преобразователей значений. Фактически, это не только устраняет необходимость их регистрации, но и фактически обеспечивает интеллектуальный смысл для выбора конвертера при вводе Converter=
. Вот статья, с которой я начал: http://www.wpftutorial.net/ValueConverters.html .
Возможность использовать расширение разметки несколько меняет баланс в моем списке плюсов и минусов и обсуждении выше (см. Зачеркивание).
В результате этого откровения я экспериментирую с гибридной системой, в которой я использую конвертеры для BoolToVisibility
того, что я называю, MatchToVisibility
и модель представления для всех других конверсий. MatchToVisibility - это в основном конвертер, который позволяет мне проверить, соответствует ли связанное значение (обычно перечисление) одному или нескольким значениям, указанным в XAML.
Пример:
Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue=Visible, IfFalse=Hidden, Value1=Finished, Value2=Canceled}}"
По сути, это проверка, является ли статус «Завершено» или «Отменено». Если это так, то видимость получает значение «Видимый». В противном случае он получает наборы «Скрытые». Это оказалось очень распространенным сценарием, и этот конвертер спас мне около 15 свойств в моей модели представления (плюс соответствующие операторы RaisePropertyChanged). Обратите внимание, что при Converter={vc:
вводе «MatchToVisibility» отображается в меню intellisense. Это заметно снижает вероятность ошибок и делает использование преобразователей значений менее утомительным (вам не нужно запоминать или искать нужное имя преобразователя значений).
Если вам интересно, я вставлю код ниже. Одной из важных особенностей данной реализации MatchToVisibility
является то , что он проверяет, если оценка стоимости является enum
, и , если он есть, он проверяет , чтобы убедиться Value1
, Value2
и т.д., также перечислений одного и того же типа. Это обеспечивает проверку времени разработки и выполнения того, были ли какие-либо из значений перечисления опечатаны. Чтобы улучшить это до проверки во время компиляции, вы можете использовать следующее (я набрал это вручную, поэтому, пожалуйста, простите меня, если я сделал какие-либо ошибки):
Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue={x:Type {win:Visibility.Visible}},
IfFalse={x:Type {win:Visibility.Hidden}},
Value1={x:Type {enum:Status.Finished}},
Value2={x:Type {enum:Status.Canceled}}"
Хотя это безопаснее, это слишком многословно, чтобы стоить для меня. Я мог бы также просто использовать свойство в модели представления, если я собираюсь сделать это. Во всяком случае, я обнаружил, что проверка во время разработки вполне подходит для сценариев, которые я пробовал до сих пор.
Вот код для MatchToVisibility
[ValueConversion(typeof(object), typeof(Visibility))]
public class MatchToVisibility : BaseValueConverter
{
[ConstructorArgument("ifTrue")]
public object IfTrue { get; set; }
[ConstructorArgument("ifFalse")]
public object IfFalse { get; set; }
[ConstructorArgument("value1")]
public object Value1 { get; set; }
[ConstructorArgument("value2")]
public object Value2 { get; set; }
[ConstructorArgument("value3")]
public object Value3 { get; set; }
[ConstructorArgument("value4")]
public object Value4 { get; set; }
[ConstructorArgument("value5")]
public object Value5 { get; set; }
public MatchToVisibility() { }
public MatchToVisibility(
object ifTrue, object ifFalse,
object value1, object value2 = null, object value3 = null,
object value4 = null, object value5 = null)
{
IfTrue = ifTrue;
IfFalse = ifFalse;
Value1 = value1;
Value2 = value2;
Value3 = value3;
Value4 = value4;
Value5 = value5;
}
public override object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
var ifTrue = IfTrue.ToString().ToEnum<Visibility>();
var ifFalse = IfFalse.ToString().ToEnum<Visibility>();
var values = new[] { Value1, Value2, Value3, Value4, Value5 };
var valueStrings = values.Cast<string>();
bool isMatch;
if (Enum.IsDefined(value.GetType(), value))
{
var valueEnums = valueStrings.Select(vs => vs == null ? null : Enum.Parse(value.GetType(), vs));
isMatch = valueEnums.ToList().Contains(value);
}
else
isMatch = valueStrings.Contains(value.ToString());
return isMatch ? ifTrue : ifFalse;
}
}
Вот код для BaseValueConverter
// this is how the markup extension capability gets wired up
public abstract class BaseValueConverter : MarkupExtension, IValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
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();
}
}
Вот метод расширения ToEnum
public static TEnum ToEnum<TEnum>(this string text)
{
return (TEnum)Enum.Parse(typeof(TEnum), text);
}
Обновление 2
С тех пор как я разместил этот вопрос, я натолкнулся на проект с открытым исходным кодом, который использует «IL-ткачество» для внедрения кода NotifyPropertyChanged для свойств и зависимых свойств. Это делает реализацию видения Джоша Смита модели представления как «преобразователя стоимости на стероидах» абсолютным бризом. Вы можете просто использовать «Автоматически реализованные свойства», а ткач сделает все остальное.
Пример:
Если я введу этот код:
public string GivenName { get; set; }
public string FamilyName { get; set; }
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
... это то, что компилируется:
string givenNames;
public string GivenNames
{
get { return givenName; }
set
{
if (value != givenName)
{
givenNames = value;
OnPropertyChanged("GivenName");
OnPropertyChanged("FullName");
}
}
}
string familyName;
public string FamilyName
{
get { return familyName; }
set
{
if (value != familyName)
{
familyName = value;
OnPropertyChanged("FamilyName");
OnPropertyChanged("FullName");
}
}
}
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
Это огромная экономия в объеме кода, который вы должны набирать, читать, прокручивать и т. Д. Однако, что более важно, это избавляет вас от необходимости выяснять, каковы ваши зависимости. Вы можете добавлять новые свойства, например, FullName
без необходимости тщательно продвигаться по цепочке зависимостей для добавления в RaisePropertyChanged()
вызовы.
Как называется этот проект с открытым исходным кодом? Первоначальная версия называется «NotifyPropertyWeaver», но владелец (Саймон Поттер) с тех пор создал платформу «Fody» для размещения целой серии ткачей IL. Эквивалент NotifyPropertyWeaver под этой новой платформой называется PropertyChanged.Fody.
- Ложные инструкции по установке: http://code.google.com/p/fody/wiki/SampleUsage (замените «Virtuosity» на «PropertyChanged»)
- Сайт проекта PropertyChanged.Fody: http://code.google.com/p/propertychanged/
Если вы предпочитаете использовать NotifyPropertyWeaver (который немного проще в установке, но не обязательно будет обновляться в будущем после исправления ошибок), вот сайт проекта: http://code.google.com/p/ notifypropertyweaver /
В любом случае, эти решения для IL-ткача полностью меняют исчисление в споре между моделью представления о стероидах и преобразователях стоимости.
источник
BooleanToVisibility
принимает одно значение, которое связано с видимостью (true / false), и переводит его в другое. Это похоже на идеальное использованиеValueConverter
. С другой стороны,MatchToVisibility
кодирует бизнес-логику вView
(какие типы элементов должны быть видны). По моему мнению, эта логика должна быть направлена внизViewModel
или даже дальше в то, что я называюEditModel
. То, что может видеть пользователь, должно быть проверено.MatchToVisibility
казалось удобным способом включить некоторые простые переключатели режимов (у меня есть одно представление, в частности, с кучей деталей, которые можно включать и выключать. В большинстве случаев участки вида даже помечаются (сx:Name
), чтобы соответствовать режиму они соответствуют.) Мне действительно не приходило в голову, что это «бизнес-логика», но я подумаю над вашим комментарием.Ответы:
Я использовал
ValueConverters
в некоторых случаях и поместил логикуViewModel
в других. Я чувствую, что aValueConverter
становится частьюView
слоя, поэтому, если логика действительно является частью,View
тогда поместите его туда, иначе поместите его вViewModel
.Лично я не вижу проблемы в
ViewModel
работе сView
-специфичными понятиями, такими какBrush
es, потому что в моих приложенияхViewModel
существует только как тестируемая и привязываемая поверхность дляView
. Тем не менее, некоторые люди вкладывают много бизнес-логики вViewModel
(я не делаю), и в этом случаеViewModel
это больше похоже на часть их бизнес-уровня, поэтому в этом случае я бы не хотел, чтобы там были специфичные для WPF вещи.Я предпочитаю другое разделение:
View
- WPF материал, иногда непроверяемый (например, XAML и кодовый код), но такжеValueConverter
иViewModel
- тестируемый и привязываемый класс, который также специфичен для WPFEditModel
- часть бизнес-уровня, которая представляет мою модель во время манипуляцииEntityModel
- часть бизнес-уровня, которая представляет мою модель как сохраненнуюRepository
- отвечает за сохранениеEntityModel
базы данныхТак, как я это делаю, у меня мало пользы
ValueConverter
сСпособ, которым я избавился от некоторых ваших «Конов», состоит в том, чтобы сделать мои
ViewModel
очень универсальными. Например, один изViewModel
них, названный,ChangeValueViewModel
реализует свойство Label и свойство Value. НаView
естьLabel
привязка к свойству Label иTextBox
привязка к свойству Value.Я тогда ,
ChangeValueView
который являетсяDataTemplate
ключом оффChangeValueViewModel
типа. Всякий раз, когда WPF видит, чтоViewModel
он применяет этоView
. Конструктор myChangeValueViewModel
берет логику взаимодействия, необходимую ему для обновления своего состоянияEditModel
(обычно просто передавая aFunc<string>
), и действие, которое необходимо выполнить, когда пользователь редактирует значение (просто aAction
, выполняющее некоторую логику вEditModel
).Родитель
ViewModel
(для экрана) принимаетEditModel
конструктор и просто создает соответствующие элементарныеViewModel
элементы, такие какChangeValueViewModel
. Поскольку родительViewModel
вводит действие, которое необходимо выполнить, когда пользователь вносит какие-либо изменения, он может перехватить все эти действия и выполнить другие действия. Следовательно, введенное действие редактированияChangeValueViewModel
может выглядеть так:Очевидно, что
foreach
цикл можно реорганизовать в другом месте, но для этого нужно выполнить действие, применить его к модели, а затем (при условии, что модель обновила свое состояние каким-то неизвестным способом), сказать всем дочернимViewModel
элементам, чтобы они пошли и получили свое состояние из модель снова. Если состояние изменилось, они несут ответственность за выполнение своихPropertyChanged
событий по мере необходимости.Это прекрасно обрабатывает взаимодействие между, скажем, списком и панелью деталей. Когда пользователь выбирает новый выбор, он обновляет его
EditModel
иEditModel
изменяет значения свойств, отображаемых для панели сведений. ВViewModel
детях, которые отвечают за отображение информации о детали панели автоматически получить уведомление , что они должны проверить новые значения, и если они изменились, они стреляют своиPropertyChanged
события.источник
ViewModel
слое. Не все согласны со мной, но это зависит от того, как работает ваша архитектура.CalendarViewModel
дляCalendarView
UserControl или aDialogViewModel
для aDialogView
). Это только мое мнение, хотя :)ViewModel
.Если преобразование связано с просмотром, например, для определения видимости объекта, определения отображаемого изображения или определения цвета кисти, я всегда помещаю конвертеры в представление.
Если это связано с бизнесом, например, определение того, должно ли поле быть замаскировано, или если у пользователя есть разрешение на выполнение действия, тогда преобразование происходит в моей ViewModel.
Из ваших примеров, я думаю , что вам не хватает большой кусок WPF:
DataTriggers
. Кажется, вы используете конвертеры для определения условных значений, но конвертеры действительно должны использоваться для преобразования одного типа данных в другой.В вашем примере выше
Я бы использовал,
DataTrigger
чтобы определить, какое изображение отображать, а неConverter
. Конвертер предназначен для преобразования одного типа данных в другой, а триггер используется для определения некоторых свойств на основе значения.Единственный раз, когда я рассмотрел бы использование Конвертера для этого, это если связанное значение действительно содержало данные изображения, и мне нужно было преобразовать его в тип данных, который пользовательский интерфейс мог бы понять. Например, если источник данных содержит свойство с именем
ImageFilePath
, чем я бы рассмотрел использование конвертера для преобразования строки, содержащей местоположение файла изображения, в объект,BitmapImage
который можно использовать в качестве источника для моего изображенияВ результате у меня есть одно пространство имен библиотеки, полное универсальных преобразователей, которые преобразуют один тип данных в другой, и мне редко приходится кодировать новый преобразователь. Бывают случаи, когда мне нужны конвертеры для конкретных конверсий, но они достаточно редки, и я не против написать их.
источник
Grid
элементы. Я также пытаюсь сделать что-то вроде установки кистей для переднего плана / фона / обводки на основе данных в моей модели представления и определенной цветовой палитры, определенной в файле конфигурации. Я не уверен, что это отлично подходит для триггера или преобразователя. Единственная проблема, с которой я сталкиваюсь до сих пор с использованием большей части логики представления в модели представления, - это связывание всехRaisePropertyChanged()
вызовов.DataTrigger
, даже выключая элементы Grid. Обычно я помещаю туда,ContentControl
где должен быть мой динамический контент, и меняю егоContentTemplate
в триггере. У меня есть пример по следующей ссылке, если вам интересно (прокрутите вниз до раздела с заголовкомUsing a DataTrigger
) rachel53461.wordpress.com/2011/05/28/…<TextBlock Text="I'm a Person" Visibility={Binding ConsumerType, Converter={vc:MatchToVisibility IfTrue=Visible, IfFalse=Hidden, Value1=Person}}"
и<TextBlock Text="I'm a Business" Visibility={Binding ConsumerType, Converter={vc:MatchToVisibility IfTrue=Visible, IfFalse=Hidden, Value1=Business}}"
Это зависит от того, что вы тестируете , если что.
Никаких тестов: перемешать Просмотреть код с ViewModel по желанию (вы всегда можете выполнить рефакторинг позже).
Тесты на ViewModel и / или ниже: используйте конвертеры.
Тесты на модельных слоях и / или ниже: смешанный просмотр кода с помощью ViewModel по желанию
ViewModel абстрагирует модель для представления . Лично я бы использовал ViewModel для кистей и т. Д. И пропускал конвертеры. Проверьте на слое (ях), где данные находятся в « чистом » виде (т. Е. Слои модели ).
источник
Visibility
,SolidColorBrush
, иThickness
.Это, вероятно, не решит все проблемы, которые вы упомянули, но есть два момента, которые следует учитывать:
Во-первых, вам нужно разместить код конвертера где-нибудь в вашей первой стратегии. Считаете ли вы эту часть представления или модель представления? Если это часть представления, почему бы не поместить свойства, относящиеся к представлению, в представление вместо модели представления?
Во-вторых, похоже, что ваш неконвертерный проект пытается изменить существующие свойства реальных объектов. Похоже, они уже реализуют INotifyPropertyChanged, так почему бы не использовать создание объекта-оболочки для конкретного вида для привязки? Вот простой пример:
источник
Иногда полезно использовать конвертер значений, чтобы воспользоваться преимуществами виртуализации.
Примером этого является то, что в проекте, где нам приходилось отображать битовые маски данных для сотен тысяч ячеек в сетке. Когда мы декодировали битовые маски в модели представления для каждой отдельной ячейки, программа загружалась слишком долго.
Но когда мы создали конвертер значений, который декодировал одну ячейку, программа загружалась за короткий промежуток времени и была так же отзывчива, потому что конвертер вызывается только тогда, когда пользователь смотрит на конкретную ячейку (и его нужно будет вызывать только максимум тридцать раз каждый раз, когда пользователь переключал свой взгляд на сетку).
Я не знаю, как MVVM жалуется на это решение, но оно сократило время загрузки на 95%.
источник