У меня есть следующая пара модель / модель просмотра. Это очень распространенная ситуация - чистое отображение из 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);
}
}
}
Почему бы не реализовать INotifyPropertyChanged непосредственно в вашей (DTO?) Модели?
@dymanoid В конце концов, это вопрос предпочтений. Мне нравится рассматривать модель как простой набор свойств, а модель просмотра имеет функцию уведомления об изменении свойств, которая в данном случае используется для запуска автосохранения в постоянный файл.





Похоже, вы ищете что-то вроде AutoMapper
Из того, что я понял, читая домашнюю страницу Automapper, вы могли бы использовать ее для сопоставления от одного экземпляра объекта к другому экземпляру объекта, в то время как мой вариант использования - сопоставить (в обоих направлениях) значение данного свойства в объекте (модели) со значением данного свойства объекта текущий объект (модель просмотра).
Нет необходимости писать 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 как предпочтительный способ достижения привязки данных без слишком большого количества шаблонного кода.
AutoMapper? Ты знаешь это?