价值转换器会带来更多麻烦吗?


20

我正在使用需要大量值转换的视图的WPF应用程序。最初,我的哲学(部分是受到有关XAML信徒的激烈辩论的启发)的,我应该严格按照支持视图数据要求的观点来构建视图模型。这意味着将值转换为可见性,画笔,大小等所需的任何值转换都将由值转换器和多值转换器处理。从概念上讲,这似乎很优雅。视图模型和视图都将具有独特的目的并且可以很好地分离。在“数据”和“外观”之间将划清界限。

好吧,在将这种策略赋予“旧的大学尝试”之后,我有些怀疑是否要继续以这种方式发展。我实际上正在强烈考虑转储值转换器,并将(几乎)所有值转换的责任直接交给视图模型。

使用价值转换器的现实似乎并没有达到完全分开的关注点的表观价值。对于价值转换器,我最大的问题是使用它们很乏味。您必须创建一个新类,实现IValueConverterIMultiValueConverter,将一个或多个值转换为object正确的类型,进行测试DependencyProperty.Unset(至少对于多值转换器而言),编写转换逻辑,在资源字典中注册转换器 [请参见下面的更新],最后,使用相当冗长的XAML(这要求转换器的绑定和名称都使用魔术字符串)来连接转换器[请参见下面的更新]。调试过程也不是一件容易的事,因为错误消息通常是不明确的,尤其是在Visual Studio的设计模式/ Expression Blend中。

这并不是说替代方案(使视图模型负责所有值转换)是一种改进。这很可能是另一面草更绿的问题。除了失去优雅的关注点分离之外,您还必须编写一堆派生属性,并确保RaisePropertyChanged(() => DerivedProperty)在设置基本属性时认真调用,这可能会带来令人不愉快的维护问题。

以下是我汇总的初始清单,这些清单允许视图模型处理转换逻辑并取消使用值转换器的优缺点:

  • 优点:
    • 由于取消了多转换器,因此总绑定数更少
    • 较少的魔术字符串(绑定路径+转换器资源名称
    • 无需再注册每个转换器(还要维护此列表)
    • 减少编写每个转换器的工作(无需实现接口或强制转换)
    • 可以轻松注入依赖项以帮助进行转换(例如颜色表)
    • XAML标记不那么冗长,更易于阅读
    • 转换器重用仍然可能(尽管需要一些计划)
    • DependencyProperty.Unset没有神秘问题(我在多值转换器中注意到的一个问题)

*删除线表示如果您使用标记扩展,这些好处将消失(请参阅下面的更新)

  • 缺点:
    • 视图模型和视图之间的耦合更强(例如,属性必须处理可见性和画笔之类的概念)
    • 更多的总体属性可允许直接映射视图中的每个绑定
    • RaisePropertyChanged必须为每个派生属性调用(请参阅下面的更新2)
    • 如果转换基于UI元素的属性,则仍必须依赖转换器

因此,正如您可能会说的那样,我对此问题感到非常沮丧。我非常不愿走重构的道路,只是意识到无论我在视图模型中使用值转换器还是暴露大量的值转换属性,编码过程都是同样低效而乏味的。

我是否缺少任何利弊?对于那些尝试了两种价值转换方式的人,您觉得哪种对您更好,为什么?还有其他选择吗?(门徒提到了有关类型描述符提供程序的一些内容,但是我无法理解他们在说什么。对此的任何见解都将受到赞赏。)


更新资料

我今天发现,可以使用一种称为“标记扩展”的东西来消除注册值转换器的需要。实际上,它不仅消除了注册它们的需要,而且还提供了在您键入时选择转换器的智能感知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”将显示在智能菜单中。这显着减少了出错的机会,并减少了使用值转换器的麻烦(您不必记住或查找所需值转换器的名称)。

如果您感到好奇,我将在下面粘贴代码。这种实现的一个重要特点MatchToVisibility是,它检查是否绑定的值是一个enum,如果是,它检查,以确保Value1Value2等也都是相同类型的枚举。这样可以在设计时和运行时检查是否有任何枚举值输入错误。为了将其改进为编译时检查,您可以改用以下代码(我手动输入了此代码,因此,如果我有任何错误,请原谅我):

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代码的开源项目。这使Josh Smith将视图模型的愿景实现为“类固醇的价值转换器”变得轻而易举。您可以只使用“自动实现的属性”,编织器将完成其余工作。

例:

如果输入此代码:

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”,但所有者(Simon Potter)此后创建了一个名为“ Fody”的平台,用于托管整个IL编织器系列。在此新平台下,NotifyPropertyWeaver的等效项称为PropertyChanged.Fody。

如果您希望使用NotifyPropertyWeaver(安装起来比较简单,但将来不一定会在错误修复后进行更新),请访问项目站点:http : //code.google.com/p/ notifypropertyweaver /

无论哪种方式,这些IL编织器解决方案都完全改变了类固醇视图模型与值转换器之间的争论。


请注意:BooleanToVisibility采用与可见性相关的一个值(是/否),然后将其转换为另一个值。这似乎是的理想用法ValueConverter。另一方面,MatchToVisibility是在中编码业务逻辑View(应该显示什么类型的项目)。在我看来,这种逻辑应该被推到ViewModel甚至是我所说的逻辑上EditModel。用户看到的应该是被测试的东西。
Scott Whitlock,

@斯科特,这是一个好点。我现在正在使用的应用程序并不是真正的“业务”应用程序,用户的权限级别不同,因此我没有考虑这些问题。MatchToVisibility似乎是启用某些简单模式切换的一种便捷方法(我有一个视图,特别是带有大量可以打开和关闭的零件。在大多数情况下,视图的各个部分甚至都标有x:Name)以匹配模式它们对应。)我并没有真正想到这是“业务逻辑”,但我会给您一些评论。
devuxer 2011年

例如:假设您有一个立体声系统,可以处于收音机,CD或MP3模式。假设在UI的不同部分中存在与每种模式相对应的视觉效果。您可以(1)让视图决定哪些图形对应于哪种模式并相应地打开/关闭它们,(2)在视图模型上针对每个模式值(例如IsModeRadio,IsModeCD)公开属性,或(3)公开每个图形元素/组在视图模型上的属性(例如IsRadioLightOn,IsCDButtonGroupOn)。(1)似乎很适合我的观点,因为它已经具有模式意识。在这种情况下,您怎么看?
devuxer 2011年

这是我在整个SE中见过的最长的问题!:]
trejder 2014年

Answers:


10

ValueConverters在某些情况下使用过,而ViewModel在其他情况下则采用了逻辑。我的感觉是a ValueConverter成为了View层的一部分,因此如果逻辑确实是the 层的一部分,View则将其放置在该层中,否则将其放置在该层中ViewModel

我个人认为ViewModel处理View诸如Brushes之类的特定概念没有问题,因为在我的应用程序中,ViewModel仅存在作为的可测试且可绑定的表面View。但是,有些人在ViewModel(我没有)中放了很多业务逻辑,在那种情况下,ViewModel它更像是他们业务层的一部分,所以在那种情况下,我不想在其中使用WPF特定的东西。

我更喜欢另一种分离:

  • View- WPF的东西,有时无法验证(如XAML和代码隐藏),而且ValueConverters ^
  • ViewModel -可测试且可绑定的类,也是WPF特定的
  • EditModel -业务层的一部分,在操作期间代表我的模型
  • EntityModel -业务层的一部分,将我的模型持久化
  • Repository-负责EntityModel数据库的持久性

因此,我这样做的方式对ValueConverters 几乎没有用

我摆脱某些“骗局”的方法是使我ViewModel的“骗局” 非常通用。例如,ViewModel我所拥有的一个ChangeValueViewModel实现了Label属性和Value属性。在上,View有一个Label绑定到Label属性,一个TextBox绑定到Value属性。

然后ChangeValueView,我得到了一个DataTemplateChangeValueViewModel类型的键控。每当WPF看到ViewModel它适用时View。我的构造函数ChangeValueViewModel采用交互逻辑,它需要从中刷新其状态EditModel(通常只是传入Func<string>),以及在用户编辑Value(它只是在Action中执行一些逻辑的EditModel)时需要采取的动作。

父级ViewModel(用于屏幕)EditModel在其构造函数中采用,并仅实例化适当的基本,ViewModel例如ChangeValueViewModel。由于父级ViewModel是在用户进行任何更改时注入要执行的操作,因此它可以拦截所有这些操作并采取其他操作。因此,为注入的编辑操作ChangeValueViewModel可能类似于:

(string newValue) =>
{
    editModel.SomeField = newValue;
    foreach(var childViewModel in this.childViewModels)
    {
        childViewModel.RefreshStateFromEditModel();
    }
}

显然,foreach循环可以在其他地方进行重构,但是这样做是要采取行动,将其应用于模型,然后(假设模型以某种未知的方式更新了其状态),告诉所有孩子ViewModel去从那里获取状态。再次模型。如果状态已更改,则他们负责PropertyChanged在必要时执行其事件。

这样可以很好地处理列表框和详细信息面板之间的交互。当用户选择一个新选项时,它将EditModel使用该选项进行更新,并EditModel更改为详细信息面板公开的属性的值。该ViewModel是负责显示详细信息面板信息自动获得通知,他们需要检查是否有新的价值观,以及他们是否已经改变,他们解雇他们的孩子PropertyChanged的事件。


/点头。这与我的模样非常相似。
伊恩

+1。感谢您的回答,Scott,我所做的工作几乎相同,而且我也没有在视图模型中放入业务逻辑。(作为记录,我使用的是EntityFramework Code First,我有一个在视图模型和实体模型之间转换的服务层,反之亦然。)因此,鉴于此,我想您是在说没有多少成本将所有/大部分转换逻辑放入视图模型层。
devuxer 2011年

@DanM-是的,我同意。我希望在该ViewModel层进行转换。并非每个人都同意我的观点,但这取决于您的体系结构如何工作。
Scott Whitlock,

2
在阅读了第一段之后,我要说+1,但是随后我读了第二段,并且非常不同意将特定于视图的代码放入ViewModels。一个例外是,如果ViewModel是专门为在通用View(例如,CalendarViewModel用于CalendarViewUserControl或DialogViewModel用于DialogView)使用的通用View之后而创建的。不过,那只是我的意见:)
Rachel

@Rachel-好吧,如果您继续阅读我的第二段,您会发现这正是我在做的事情。:)我ViewModel的业务中没有业务逻辑。
Scott Whitlock

8

如果“转换”是与视图相关的,例如确定对象的可见性,确定要显示的图像或弄清楚要使用的画笔颜色,则我总是将转换器放在视图中。

如果与业务相关,例如确定是否应屏蔽字段,或者用户是否有权执行操作,则转换将在我的ViewModel中进行。

从您的示例中,我认为您缺少了很多WPF :DataTriggers。您似乎正在使用转换器来确定条件值​​,但是转换器实际上应该用于将一种数据类型转换为另一种数据类型。

在上面的例子中

例如:假设您有一个立体声系统,可以处于收音机,CD或MP3模式。假设在UI的不同部分中存在与每种模式相对应的视觉效果。您可以(1)让视图决定哪些图形对应于哪种模式并相应地打开/关闭它们,(2)在视图模型上针对每个模式值(例如IsModeRadio,IsModeCD)公开属性,或(3)公开每个图形元素/组在视图模型上的属性(例如IsRadioLightOn,IsCDButtonGroupOn)。(1)似乎很适合我的观点,因为它已经具有模式意识。在这种情况下,您怎么看?

我会用DataTrigger确定图片,而不是确定Converter。转换器用于将一种数据类型转换为另一种数据类型,而触发器用于基于值确定某些属性。

<Style x:Key="RadioImageStyle">
    <Setter Property="Source" Value="{StaticResource RadioImage}" />
    <Style.Triggers>
        <DataTrigger Binding="{Binding Mode}" Value="CD">
            <Setter Property="Source" Value="{StaticResource CDImage}" />
        </DataTrigger>
        <DataTrigger Binding="{Binding Mode}" Value="MP3">
            <Setter Property="Source" Value="{StaticResource MP3Image}" />
        </DataTrigger>
    </Style.Triggers>
</Style>

我唯一会考虑使用Converter的方法是,绑定值是否实际包含图像数据,并且需要将其转换为UI可以理解的数据类型。例如,如果数据源包含一个名为的属性ImageFilePath,那么我会考虑使用Converter将包含图像文件位置的字符串转换为BitmapImage,可以用作我的Image的源

<Style x:Key="RadioImageStyle">
    <Setter Property="Source" Value="{Binding ImageFilePath, 
            Converter={StaticResource StringPathToBitmapConverter}}" />
</Style>

最终结果是我有一个充满通用转换器的库名称空间,这些转换器将一种数据类型转换为另一种数据类型,而我几乎不必编写新的转换器。在某些情况下,我希望转换器进行特定的转换,但是它们很少出现,所以我不介意编写它们。


+1。您提出了一些好点。我以前使用过触发器,但就我而言,我没有切换图像源(它是一个属性),而是切换了整个Grid元素。我还尝试根据我的视图模型中的数据和配置文件中定义的特定调色板来设置前景/背景/描边画笔。我不确定这是否适合触发器或转换器。到目前为止,我将大多数视图逻辑放入视图模型的唯一问题是连接所有RaisePropertyChanged()调用。
devuxer 2011年

@DanM我实际上会在中做所有这些事情DataTrigger,甚至切换出Grid的元素。通常,我将ContentControl动态内容放置在应有的位置并ContentTemplate在触发器中换出。如果您有兴趣,请在以下链接中找到示例(向下滚动到标题为的部分Using a DataTriggerrachel53461.wordpress.com/2011/05/28/…–
Rachel

我以前使用过数据模板和内容控件,但是我从来不需要触发器,因为我一直对每个视图都有唯一的视图模型。无论如何,您的技术很合理,很优雅,但是也很冗长。:随着MatchToVisibility,它可以缩短这个<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}}"
devuxer

1

这取决于您要测试的内容(如果有)。

没有测试:随意混合带有ViewModel的View代码(您以后可以随时重构)。
在ViewModel和/或更低版本上进行测试:使用转换器。
在模型层和/或更低层上进行测试:随意混合带有ViewModel的View代码

ViewModel提取View的模型。就个人而言,我将ViewModel用于Brushes等,并跳过转换器。 测试,其中层(一个或多个)上的数据是在其“ 最纯 ”的形式(即,模型的层)。


2
关于测试的有趣观点,但是我想我没有看到视图模型中的转换器逻辑如何损害视图模型的可测试性?我当然不建议在视图模型中放置实际的UI 控件。只要查看特定的属性,如VisibilitySolidColorBrushThickness
devuxer 2011年

@DanM:如果您使用的是“ 视图优先”方法,那就没问题了。但是,有些使用ViewModel-first方法(其中ViewModel引用View的方法)可能会出现问题
杰克·伯杰

嗨,周杰伦,绝对是视图优先的方法。除了需要绑定到的属性名称之外,视图对视图模型一无所知。感谢您的跟进。+1。
devuxer 2011年

0

这可能无法解决您提到的所有问题,但是需要考虑两点:

首先,您需要将转换器代码放置在第一个策略中的某个位置。您是否考虑视图或视图模型的那一部分?如果它是视图的一部分,为什么不将视图特定的属性而不是视图模型放置在视图中?

其次,听起来您的非转换器设计试图修改已经存在的实际对象属性。听起来他们已经实现了INotifyPropertyChanged,那么为什么不使用create view-specific wrapper object来绑定呢?这是一个简单的例子:

public class RealData
{
    private bool mIsInteresting;
    public bool IsInteresting
    {
        get { return mIsInteresting; }
        set 
        {
            if (mIsInteresting != null) 
            {
                mIsInteresting = value;
                RaisePropertyChanged("IsInteresting");
            }
        }
    }
}

public class RealDataView
{
    private RealData mRealData;

    public RealDataView(RealData data)
    {
        mRealData = data;
        mRealData.PropertyChanged += OnRealDataPropertyChanged;
    }

    public Visibility IsVisiblyInteresting
    {
       get { return mRealData.IsInteresting ? Visibility.Visible : Visibility.Hidden; }
       set { mRealData.IsInteresting = (value == Visibility.Visible); }
    }

    private void OnRealDataPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "IsInteresting") 
        {
            RaisePropertyChanged(this, "IsVisiblyInteresting");
        }
    }
}

我并不是要暗示我要直接在视图或视图模型中更改实体模型的属性。视图模型绝对是与我的实体模型层不同的层。实际上,到目前为止,我已经完成了只读视图上的工作。这并不是说我的应用程序不会进行任何编辑,但是我看不到用于编辑的控件上进行了任何转换(因此,假定除了列表中的选择之外,所有绑定都是单向的)。关于“数据视图”的好处。这是我在问题顶部提到的XAML徒弟帖子中提出的一个概念。
devuxer 2011年

0

有时,最好使用值转换器来利用虚拟化。

在一个项目中,我们不得不显示网格中成千上万个单元的位掩码数据,这就是一个例子。当我们解码视图模型中每个单元的位掩码时,程序花费的时间太长而无法加载。

但是,当我们创建一个对单个单元格进行解码的值转换器时,程序会在很短的时间内加载,并且响应速度很快,因为仅在用户查看特定单元格时才调用该转换器(并且只需要调用该转换器即可)用户每次在网格上移动其视图时,最多可以进行三十次)。

我不知道MVVM如何抱怨该解决方案,但是它将加载时间减少了95%。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.