heltonbiker
heltonbiker

Reputation: 27605

Is there a way to automatically create or map ViewModel properties to Model properties?

I have the following model/viewmodel pair. This is a very common situation - pure mapping from ViewModel to Model properties - and contain a lot of repeated and error-prone code.

I would like to know if there is some better way to do it, specifically to reduce the chance of error (forgetting a property, using the wrong property name).

More recent language features, like CallingMemberName are welcome, but currently I am not sure I understand them.


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);
        }
    }
}

Upvotes: 3

Views: 2083

Answers (5)

Matteo Sganzetta
Matteo Sganzetta

Reputation: 788

I'm a fan of AutoMapper both for earlier development phase (because it allows quick changes and refactoring) and for projects where performances are not an issue. Otherwise, when performance are critical and the model/viewmodel structure is somehow defined and stable, I prefer to switch to manual mapping adding extension methods on model and viewmodel (on a project that has reference on both of them) with the help of this tool to automate writing code MappingGenerator

Upvotes: 0

William Forsdal
William Forsdal

Reputation: 1

I stumbled across this question so I thought I would add how I solve this issue. I have a MyViewModelBase type that wraps my MyModel. Both classes implement INotifyPropertyChanged, and the ViewModel just forwards PropertyChanged events, like so:

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; }
}

We have many different models and view models that inherit from these two base classes. To get change notifications for a property in the model, simply add a wrapper for it with the same name in the view model, and when the property in the model changes, the change will propogate through the view model as well.

Note that we use this in a limited part of our application, where it fits well. I don't see it scaling to every part of a large application. Use it where it fits.

Now when it comes to automatically generating wrapper properties for the underlying model, your best bet (AFAIK) is Fody. I looked quickly and found this: https://github.com/tom-englert/AutoProperties.Fody . Not sure if you could use it for that, but it was the closest thing I could find.

When C# 9/.NET 5 is released, Source Generators could also be an option.

Upvotes: 0

Ald
Ald

Reputation: 1

If using generated code is ok, then T4 Text Templates can be used to generate all properties in the ViewModel . Create an attribute to hold the Model type:

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

    }
}

Add this to the VM, and make it partial:

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

The following T4 is a short version of what I use, but I'm sure there are better ways to do it since I'm not an expert:

<#@ 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("}");
        }
    }
#>

This gets all the class declarations in your project which have the Views attribute and generates the code for each property. The generated class is

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);
            }
        }

    }
}

There are a lot of changes you can add, for example instead of using a custom attribute generate the code for all classes which inherit from ConfiguracoesViewModel instead. You can also check if each property hast been added to the VM already and not generate them, which allows you to create custom getters and setters for the properties you want by simply adding it to your class.

Upvotes: 0

Peregrine
Peregrine

Reputation: 4556

There is no need to write the ViewModel as a facade on top of the Model.

Have the Model implement INotifyPropertyChanged, either directly or using a library such as Fody.PropertyChanged. Then publish the whole Model as a single property of the ViewModel and bind to that in your View.

I covered this exact topic in my blog - Model / ViewModel.

Upvotes: 1

IEnjoyEatingVegetables
IEnjoyEatingVegetables

Reputation: 1030

It sounds like you are looking for something like AutoMapper

Upvotes: 2

Related Questions