有没有一种方法可以自动创建ViewModel属性或将其映射到Model属性?

时间:2018-08-06 15:39:14

标签: c# wpf mvvm dry

我有以下一对模型/视图模型。这是一种非常普遍的情况-从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);
        }
    }
}

4 个答案:

答案 0 :(得分:2)

听起来您正在寻找类似AutoMapper

的东西

答案 1 :(得分:1)

无需将ViewModel编写为模型顶部的外观。

让模型直接或使用诸如Fody.PropertyChanged之类的库实现INotifyPropertyChanged。然后将整个模型作为ViewModel的单个属性发布,并绑定到您的View中。

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

答案 2 :(得分:0)

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

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

    }
}

将此添加到VM,并使其部分化:

[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。

答案 3 :(得分:0)

我偶然发现了这个问题,所以我想我应该补充一下如何解决这个问题。我有一个MyViewModelBase类型,用于包装我的MyModel。这两个类都实现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时,也可以选择源生成器。