Есть ли способ автоматически создавать или сопоставлять свойства ViewModel со свойствами модели?

У меня есть следующая пара модель / модель просмотра. Это очень распространенная ситуация - чистое отображение из ViewModel в свойства модели - и содержит много повторяющегося и подверженного ошибкам кода.

Я хотел бы знать, есть ли лучший способ сделать это, в частности, чтобы уменьшить вероятность ошибки (забыть свойство, использовать неправильное имя свойства).

Более свежие языковые функции, такие как 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);
        }
    }
}

AutoMapper? Ты знаешь это?

Antoine V 06.08.2018 17:40

Почему бы не реализовать INotifyPropertyChanged непосредственно в вашей (DTO?) Модели?

dymanoid 06.08.2018 17:44

@dymanoid В конце концов, это вопрос предпочтений. Мне нравится рассматривать модель как простой набор свойств, а модель просмотра имеет функцию уведомления об изменении свойств, которая в данном случае используется для запуска автосохранения в постоянный файл.

heltonbiker 06.08.2018 20:58
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
3
857
4

Ответы 4

Похоже, вы ищете что-то вроде AutoMapper

Из того, что я понял, читая домашнюю страницу Automapper, вы могли бы использовать ее для сопоставления от одного экземпляра объекта к другому экземпляру объекта, в то время как мой вариант использования - сопоставить (в обоих направлениях) значение данного свойства в объекте (модели) со значением данного свойства объекта текущий объект (модель просмотра).

heltonbiker 06.08.2018 20:55

Нет необходимости писать ViewModel как фасад поверх модели.

Попросите Модель реализовать INotifyPropertyChanged либо напрямую, либо с помощью библиотеки, такой как Fody.PropertyChanged. Затем опубликуйте всю модель как одно свойство ViewModel и привяжите к нему в своем представлении.

Я освещал именно эту тему в своем блоге - Модель / ViewModel.

Если использование сгенерированного кода в порядке, то Текстовые шаблоны T4 можно использовать для генерации всех свойств в 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. Вы также можете проверить, было ли каждое свойство уже добавлено в виртуальную машину, и не генерировать их, что позволяет вам создавать пользовательские методы получения и установки для нужных свойств, просто добавляя его в свой класс.

Я наткнулся на этот вопрос, поэтому решил добавить, как решить эту проблему. У меня есть тип 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, генераторы исходного кода также могут быть опцией.

В настоящее время я использую комбинацию Fody и ReactiveUI для MVVM как предпочтительный способ достижения привязки данных без слишком большого количества шаблонного кода.

heltonbiker 08.06.2020 20:28

Другие вопросы по теме