【问题标题】:Is there a way to automatically create or map ViewModel properties to Model properties?有没有办法自动创建 ViewModel 属性或将其映射到 Model 属性?
【发布时间】:2019-01-13 15:01:21
【问题描述】:

我有以下模型/视图模型对。这是一种非常常见的情况——从 ViewModel 到 Model 属性的纯映射——并且包含大量重复且容易出错的代码。

我想知道是否有更好的方法来做到这一点,特别是减少出错的机会(忘记属性,使用错误的属性名称)。

欢迎使用更多最新的语言功能,例如 CallingMemberName,但目前我不确定我是否理解它们。


public class ParametrosGeometricos
{
    public double DistanciaProjetorParede { get; set; } = 2280;
    public double AlturaProjetor { get; set; } = 1000;
    public double AlturaInferiorProjecao { get; set; } = 1010;
    public double AlturaSuperiorProjecao { get; set; } = 1940;

    public double DistanciaCameraParede { get; set; } = 2320;
    public double AlturaCamera { get; set; } = 1770;
    public double AlturaInferiorImagem { get; set; } = 860;
    public double AlturaSuperiorImagem { get; set; } = 1740;
}

public class ParametrosGeometricosViewModel : ConfiguracoesViewModel<ParametrosGeometricos>
{

    // (...)


    public double DistanciaProjetorParede
    {
        get => Model.DistanciaProjetorParede;
        set
        {
            Model.DistanciaProjetorParede = value;
            RaisePropertyChanged(() => DistanciaProjetorParede);
        }
    }

    public double AlturaProjetor
    {
        get => Model.AlturaProjetor;
        set
        {
            Model.AlturaProjetor = value;
            RaisePropertyChanged(() => AlturaProjetor);
        }
    }

    public double AlturaInferiorProjecao
    {
        get => Model.AlturaInferiorProjecao;
        set
        {
            Model.AlturaInferiorProjecao = value;
            RaisePropertyChanged(() => AlturaInferiorProjecao);
        }
    }

    public double AlturaSuperiorProjecao
    {
        get => Model.AlturaSuperiorProjecao;
        set
        {
            Model.AlturaSuperiorProjecao = value;
            RaisePropertyChanged(() => AlturaSuperiorProjecao);
        }
    }



    public double DistanciaCameraParede
    {
        get => Model.DistanciaCameraParede;
        set
        {
            Model.DistanciaCameraParede = value;
            RaisePropertyChanged(() => DistanciaCameraParede);
        }
    }

    public double AlturaCamera
    {
        get => Model.AlturaCamera;
        set
        {
            Model.AlturaCamera = value;
            RaisePropertyChanged(() => AlturaCamera);
        }
    }

    public double AlturaInferiorImagem
    {
        get => Model.AlturaInferiorImagem;
        set
        {
            Model.AlturaInferiorImagem = value;
            RaisePropertyChanged(() => AlturaInferiorImagem);
        }
    }

    public double AlturaSuperiorImagem
    {
        get => Model.AlturaSuperiorImagem;
        set
        {
            Model.AlturaSuperiorImagem = value;
            RaisePropertyChanged(() => AlturaSuperiorImagem);
        }
    }
}

【问题讨论】:

  • 自动映射器?你知道吗?
  • 为什么不在你的(DTO?)模型中直接实现INotifyPropertyChanged
  • @dymanoid 最后这是一个偏好问题。我喜欢将模型视为一个简单的属性包,并且视图模型具有通知属性更改的功能 - 在这种情况下用于触发自动保存到持久文件。

标签: c# wpf mvvm dry


【解决方案1】:

对于早期开发阶段(因为它允许快速更改和重构)和性能不是问题的项目,我都是 AutoMapper 的粉丝。否则,当性能至关重要并且模型/视图模型结构以某种方式定义且稳定时,我更喜欢在此工具的帮助下切换到手动映射,在模型和视图模型(在对它们都有引用的项目上)添加扩展方法自动编写代码MappingGenerator

【讨论】:

  • AutoMapper 可以在一毫秒内映射数百个对象,因此性能必须非常关键才能使手动映射有意义。
【解决方案2】:

我偶然发现了这个问题,所以我想我会补充一下我如何解决这个问题。我有一个包裹我的MyModelMyViewModelBase 类型。这两个类都实现了INotifyPropertyChanged,ViewModel 只是转发 PropertyChanged 事件,如下所示:

public class MyViewModelBase : INotifyPropertyChanged
{
    public int MyProperty 
    {
        get => _model.MyProperty;
        set => _model.MyProperty = value;
    }

    public MyViewModelBase(MyModel model) 
    {
        // we name wrapper properties the same as the model,
        // and here we just forward the property changed notifications
        model.PropertyChanged += (sender, e) => PropertyChanged?.Invoke(this, e);
    }
    ...
}
public class MyModel : INotifyPropertyChanged
{
    // We use fody to raise property changed, 
    // but can be raised normally here otherwise
    public int MyProperty { get; set; }
}

我们有许多从这两个基类继承的不同模型和视图模型。要获取模型中属性的更改通知,只需在视图模型中为其添加同名的包装器,当模型中的属性发生更改时,更改也会通过视图模型传播。

请注意,我们在应用程序的有限部分使用它,它非常适合。我没有看到它扩展到大型应用程序的每个部分。在适合的地方使用它。

现在,当涉及到为底层模型自动生成包装器属性时,您最好的选择 (AFAIK) 是 Fody。我快速查看并发现:https://github.com/tom-englert/AutoProperties.Fody。不确定你是否可以使用它,但这是我能找到的最接近的东西。

当 C# 9/.NET 5 发布时,源生成器也可能是一个选项。

【讨论】:

  • 现在我将 Fody 和 ReactiveUI 结合用于 MVVM 作为实现数据绑定的首选方式,而无需太多样板代码。
【解决方案3】:

如果使用生成的代码没问题,那么T4 Text Templates 可以用来生成 ViewModel 中的所有属性。创建一个属性来保存模型类型:

[AttributeUsage(AttributeTargets.Class)]
public class ViewsAttribute : Attribute {
    public ViewsAttribute(Type type) {

    }
}

将此添加到虚拟机,并使其成为部分:

[Views(typeof(ParametrosGeometricos))]
partial class ParametrosGeometricosViewModel {
    (...)
}

以下 T4 是我使用的简短版本,但我确信有更好的方法可以做到这一点,因为我不是专家:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="$(SolutionDir)\packages\System.ValueTuple.4.3.0\lib\netstandard1.0\System.ValueTuple.dll" #>
<#@ assembly name="$(SolutionDir)\packages\System.Collections.Immutable.1.3.1\lib\netstandard1.0\System.Collections.Immutable.dll" #>
<#@ assembly name="$(SolutionDir)\packages\Microsoft.CodeAnalysis.Common.2.8.2\lib\netstandard1.3\Microsoft.CodeAnalysis.dll" #>
<#@ assembly name="$(SolutionDir)\packages\Microsoft.CodeAnalysis.CSharp.2.8.2\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.dll" #>
<#@ assembly name="System.Runtime" #>
<#@ assembly name="System.Text.Encoding" #>
<#@ assembly name="System.Threading.Tasks" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="Microsoft.CodeAnalysis" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp.Syntax" #>
<# 
    var solutionPath = Host.ResolveAssemblyReference("$(ProjectDir)");
    var files = Directory.GetFiles(solutionPath,"*.cs",SearchOption.AllDirectories);

    IEnumerable<ClassDeclarationSyntax> syntaxTrees = files.Select(x => CSharpSyntaxTree.ParseText(File.ReadAllText(x))).Cast<CSharpSyntaxTree>().SelectMany(c => c.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>());

    foreach(ClassDeclarationSyntax declaration in syntaxTrees.Where(x => (x.AttributeLists != null && x.AttributeLists.Count > 0 && x.AttributeLists.SelectMany(y => y.Attributes.Where(z=> z.Name.ToString()=="Views")).Any()))) {
        SyntaxNode namespaceNode = declaration.Parent;
        Write("\n\n");

        while(namespaceNode != null && !(namespaceNode is NamespaceDeclarationSyntax)) {
            namespaceNode = namespaceNode.Parent;
        }

        if(namespaceNode != null) {
            WriteLine("namespace " + ((NamespaceDeclarationSyntax)namespaceNode).Name.ToString() + " {");
        }

        string modelName= declaration.AttributeLists.SelectMany(y => y.Attributes.Where(z=> z.Name.ToString()=="Views")).First().ArgumentList.Arguments.ToString();
        modelName = modelName.Substring(7, modelName.Length-8);

        ClassDeclarationSyntax modelClass = syntaxTrees.Where(x => x.Identifier.ToString() == modelName).First();

        WriteLine("    public partial class " + declaration.Identifier.Text + " {");

        foreach(PropertyDeclarationSyntax prp in modelClass.DescendantNodes().OfType<PropertyDeclarationSyntax>()){
            WriteLine("        public " + prp.Type + " " + prp.Identifier + " {");
            WriteLine("            get => Model." + prp.Identifier + ";");
            WriteLine("            set");
            WriteLine("            {");
            WriteLine("                Model." + prp.Identifier + " = value;");
            WriteLine("                RaisePropertyChanged(() => " + prp.Identifier + ");");
            WriteLine("            }");
            WriteLine("        }\n");
        }

        WriteLine("    }");

        if(namespaceNode != null) {
            Write("}");
        }
    }
#>

这将获取项目中具有Views 属性的所有类声明,并为每个属性生成代码。生成的类是

namespace TTTTTest {
    public partial class ParametrosGeometricosViewModel {
        public double DistanciaProjetorParede {
            get => Model.DistanciaProjetorParede;
            set
            {
                Model.DistanciaProjetorParede = value;
                RaisePropertyChanged(() => DistanciaProjetorParede);
            }
        }

        public double AlturaProjetor {
            get => Model.AlturaProjetor;
            set
            {
                Model.AlturaProjetor = value;
                RaisePropertyChanged(() => AlturaProjetor);
            }
        }

        public double AlturaInferiorProjecao {
            get => Model.AlturaInferiorProjecao;
            set
            {
                Model.AlturaInferiorProjecao = value;
                RaisePropertyChanged(() => AlturaInferiorProjecao);
            }
        }

        public double AlturaSuperiorProjecao {
            get => Model.AlturaSuperiorProjecao;
            set
            {
                Model.AlturaSuperiorProjecao = value;
                RaisePropertyChanged(() => AlturaSuperiorProjecao);
            }
        }

        public double DistanciaCameraParede {
            get => Model.DistanciaCameraParede;
            set
            {
                Model.DistanciaCameraParede = value;
                RaisePropertyChanged(() => DistanciaCameraParede);
            }
        }

        public double AlturaCamera {
            get => Model.AlturaCamera;
            set
            {
                Model.AlturaCamera = value;
                RaisePropertyChanged(() => AlturaCamera);
            }
        }

        public double AlturaInferiorImagem {
            get => Model.AlturaInferiorImagem;
            set
            {
                Model.AlturaInferiorImagem = value;
                RaisePropertyChanged(() => AlturaInferiorImagem);
            }
        }

        public double AlturaSuperiorImagem {
            get => Model.AlturaSuperiorImagem;
            set
            {
                Model.AlturaSuperiorImagem = value;
                RaisePropertyChanged(() => AlturaSuperiorImagem);
            }
        }

    }
}

您可以添加很多更改,例如,代替使用自定义属性,为所有继承自 ConfiguracoesViewModel 的类生成代码。您还可以检查每个属性是否已经添加到 VM 中而不生成它们,这允许您通过简单地将其添加到您的类中来为您想要的属性创建自定义 getter 和 setter。

【讨论】:

    【解决方案4】:

    没有必要将 ViewModel 写为 Model 之上的外观。

    让模型直接实现 INotifyPropertyChanged,或者使用 Fody.PropertyChanged 等库。然后将整个模型发布为 ViewModel 的单个属性并绑定到您的视图中。

    我在我的博客中介绍了这个确切的主题 - Model / ViewModel

    【讨论】:

      【解决方案5】:

      听起来你正在寻找类似AutoMapper的东西

      【讨论】:

      • 根据我阅读 Automapper 主页的理解,您将使用它来映射 一个对象实例到另一个对象实例,而我的用例是映射(双向)对象(模型)中给定属性的值与当前对象(视图模型)的给定属性的值。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-25
      • 2017-11-02
      • 2011-01-11
      • 1970-01-01
      相关资源
      最近更新 更多