#wpf #mvvm
#wpf #mvvm
Вопрос:
В настоящее время я работаю над приложением, все объекты которого поступают прямо из Linq2SQL. Пытаясь найти способ сохранить все четко разделенным, я добавил модель домена для типа, который пришел из Linq2SQL, и обернул это ViewModel.
Это немного усложнило мой код, поскольку у меня есть уровень сервиса, и в нем при инициализации я получил объект из L2S, обновил свой класс домена и заполнил его любыми данными, которые были в объекте.
Когда я хотел вставить элемент обратно в базу данных в L2S, я столкнулся с проблемой, которая заставила меня сделать обратное: заполнить объект данными из класса domain. В этот момент я начал сомневаться, на правильном ли я пути, поэтому я начал думать о том, что я сделал неправильно, или просто, возможно, думал, что это правильный путь, но в конечном итоге это не так.
В итоге я подумал, что обертывание объекта вместо заполнения модели домена и обертывания этого, возможно, было правильным решением в этой ситуации. Если бы я не стал делать что-то подобное, мне нужно было бы иметь инструкцию using в ViewModel моего представления, указывающую на DAL. Возможно, я ошибаюсь, но, насколько я читал (в книгах, статьях в Интернете), это не является четким разделением проблем.
Итак, отсюда мой вопрос:
Когда нет моделей предметной области, используется ли ViewModel для модели, чтобы исключить DAL из ViewModel представления?
Ответ №1:
Ваш вопрос немного расплывчатый. Однако я вижу много путаницы в сообществе по поводу шаблона MVVM. Многие люди «оборачивают» модель в ViewModel, как то, что вы делаете, и это неправильно.
Модель — это в основном ваши объекты L2S.
ViewModel напрямую предоставляет объекты L2S и обрабатывает логику взаимодействия, то есть обрабатывает команды, обновления объектов и т.д. (ничего не переносит, просто как ссылку на них).
Обертывание неверно, допустим, например, что вы хотите что-то слишком сложное для конвертера, но вам нужно иметь это в вашей сущности (BusinessObject или что-то еще, выходящее из L2S — вашей ORM-платформы), вам следует расширить свою сущность для поддержки этого.
Я приведу вам несколько примеров, однако они взяты из немного другой архитектуры:
- Entity Framework 4.1
- Prism, реализация MVVM
- Контейнер для внедрения зависимостей MEF
Это список задач в этом приложении, конечным результатом является проект, подобный представлению, с диаграммой Ганта.
ViewModel выглядит следующим образом:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using Sigep.Common.Interfaces;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Prism.ViewModel;
using System.Windows.Input;
using System.Diagnostics;
using Sigep.Common.DataSelfTracking;
using System.Windows.Data;
using System.Globalization;
using System.Windows;
using System.Collections.ObjectModel;
using System.Windows.Media;
using System.Threading;
using System.Threading.Tasks;
namespace Sigep.WPF.Controls.Cronograma
{
[Export(typeof(TarefasListViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class TarefasListViewModel : NotificationObject
{
private IDataManager _data;
private IRegionManager _regionManager;
private static ObservableCollection<Tarefa> _tarefas;
private static List<Sigep.Common.DataSelfTracking.Tarefa> _tarefasList;
[ImportingConstructor]
public TarefasListViewModel(IRegionManager regionManager, IDataManager data)
{
_data = data;
_regionManager = regionManager;
_tarefas = new ObservableCollection<Tarefa>(_data.TarefaList.OrderBy(T => T.Codigo));
_tarefasList = _data.TarefaList;
_data.Loaded = new EventHandler<DataManagerEventArgs>(Data_Loaded);
_NovaTarefa_Command = new DelegateCommand(this.NovaTarefa);
}
void Data_Loaded(object sender, DataManagerEventArgs e)
{
Task.Factory.StartNew(() =>
{
_tarefas.Clear();
foreach (var tarefa in _data.TarefaList.OrderBy(T => T.Codigo)) _tarefas.Add(tarefa);
RaisePropertyChanged(() => this.Promotores);
}
, CancellationToken.None
, TaskCreationOptions.None
, Indra.Injection.ServiceLocator.MefContainer.GetExportedValue<TaskScheduler>());
}
public ObservableCollection<Tarefa> Tarefas
{
get
{
return _tarefas;
}
}
public static void refreshTarefas()
{
_tarefas.Clear();
foreach (var tarefa in _tarefasList.OrderBy(T => T.Codigo)) _tarefas.Add(tarefa);
}
public IEnumerable<PromotorIndex> Promotores
{
get
{
int index = 0;
foreach (var promotor in _data.Candidatura.Promotores.OrderBy(p => p.Nome))
{
yield return new PromotorIndex()
{
Nome = promotor.Nome,
Index = index
};
}
}
}
private ICommand _NovaTarefa_Command;
public ICommand NovaTarefa_Command { get { return this._NovaTarefa_Command; } }
private void NovaTarefa()
{
Tarefa tarefa = new Tarefa { Inicio = DateTime.Now, Fim = DateTime.Now, Nome = "",Actividade = _data.ActividadeList.FirstOrDefault() };
IRegionManager _region = Indra.Injection.ServiceLocator.MefContainer.GetExportedValue<IRegionManager>();
foreach (var v in _region.Regions["CrudCronogramaTarefas"].Views) _region.Regions["CrudCronogramaTarefas"].Remove(v);
_region.Regions["CrudCronogramaTarefas"].RequestNavigate("/TarefaDefault", nr => { });
var view = ((FrameworkElement)_region.Regions["CrudCronogramaTarefas"].ActiveViews.FirstOrDefault());
((UserControlCrudBase)view).Permissions = Sigep.WPF.Controls.UserControlCrudBase.PermissionsType.Update;
view.DataContext = tarefa;
}
}
public class PromotorIndex
{
public string Nome { get; set; }
public int Index { get; set; }
}
// Converters
public class DataInicioPercentagemConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var tarefa = (Tarefa)value;
var candidatura = Indra.Injection.ServiceLocator.MefContainer.GetExportedValue<IDataManager>().Candidatura;
double totalDias = candidatura.DataFim.Subtract(candidatura.DataInicio).Days;
double diasTarefaInicio = tarefa.Inicio.Subtract(candidatura.DataInicio).Days;
return new GridLength((diasTarefaInicio / totalDias * 100), GridUnitType.Star);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
public class DataMeioPercentagemConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var tarefa = (Tarefa)value;
var candidatura = Indra.Injection.ServiceLocator.MefContainer.GetExportedValue<IDataManager>().Candidatura;
double totalDias = candidatura.DataFim.Subtract(candidatura.DataInicio).Days;
double diasTarefa = tarefa.Fim.Subtract(tarefa.Inicio).Days;
return new GridLength((diasTarefa / totalDias * 100), GridUnitType.Star);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
public class DataFimPercentagemConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var tarefa = (Tarefa)value;
var candidatura = Indra.Injection.ServiceLocator.MefContainer.GetExportedValue<IDataManager>().Candidatura;
double totalDias = candidatura.DataFim.Subtract(candidatura.DataInicio).Days;
double diasTarefaFim = candidatura.DataFim.Subtract(tarefa.Fim).Days;
return new GridLength((diasTarefaFim / totalDias * 100), GridUnitType.Star);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
[ValueConversion(typeof(int), typeof(Brush))]
public class IndexColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(Brush))
throw new InvalidOperationException("The target must be a Brush");
switch((int)value)
{
case 0:
return Brushes.Red;
case 1:
return Brushes.Green;
case 2:
return Brushes.Blue;
case 3:
return Brushes.Purple;
case 4:
return Brushes.Yellow;
case 5:
return Brushes.Brown;
default:
return Brushes.Pink;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(int))
throw new InvalidOperationException("The target must be a int");
var color = (Brush)value;
if (color == Brushes.Red) return 0;
else if (color == Brushes.Green) return 1;
else if (color == Brushes.Blue) return 2;
else if (color == Brushes.Purple) return 3;
else if (color == Brushes.Yellow) return 4;
else if (color == Brushes.Brown) return 5;
else return -1;
}
}
}
Как вы можете видеть, там есть специальные преобразователи, некоторые объекты, которые мы используем для создания дополнительной функциональности пользовательского интерфейса, и обрабатывает взаимодействие с командами непосредственно из представления.
Задача (здесь называемая Tarefa из португальского названия) сама по себе является объектом самоконтроля Entity Framework, и она расширена путем добавления второго частичного определения класса. Сам шаблон STE T4 не так уж сильно изменен, большая часть настройки выполняется путем добавления дополнительного частичного определения класса к объектам, которые мы хотим настроить.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sigep.Common.DataSelfTracking.Utils;
namespace Sigep.Common.DataSelfTracking
{
[System.Diagnostics.DebuggerDisplay("{Codigo}")]
public partial class Tarefa
{
private const double GRID_HEIGHT = 32;
public Actividade ActividadeNum
{
get
{
return Actividade;
}
set
{
Actividade = value;
this.Numero = this.Actividade.Tarefas.Max(X=> X.Numero) 1;
}
}
public Candidatura CandidaturaActual
{
get
{
if (RelsRecursosInternosTarefas.Count > 0)
return this.RelsRecursosInternosTarefas[0].RecursoInterno.Estabelecimento.Promotor.Candidatura;
else if (RelsRecursosExternosTarefas.Count > 0)
return this.RelsRecursosExternosTarefas[0].RecursoExterno.EntidadeExterna.Promotores[0].Candidatura;
else
return null;
}
}
public double TotalHoras
{
get
{
return RelsRecursosInternosTarefas.Sum(r => r.Duracao) RelsRecursosExternosTarefas.Sum(r => r.Duracao);
}
}
public string Codigo
{
get
{
return Actividade.PKID.ToString() "." Numero;
}
}
public int DuracaoDias
{
get
{
return (int)Math.Ceiling(DateTimeUtils.CalculateBusinessDays(Inicio, Fim));
}
}
public IEnumerable<AlocacaoPromotorTarefa> PercentagensParticipacao
{
get
{
var altura = GRID_HEIGHT / CandidaturaActual.Promotores.Count;
int index = 0;
foreach (var promotor in CandidaturaActual.Promotores.OrderBy(p => p.Nome))
{
var totalRI = RelsRecursosInternosTarefas.Where(r => r.RecursoInterno.Estabelecimento.Promotor == promotor).Sum(r => r.Duracao);
var totalRE = RelsRecursosExternosTarefas.Where(r => r.RecursoExterno.Estabelecimento.Promotor == promotor).Sum(r => r.Duracao);
yield return new AlocacaoPromotorTarefa() {
Actual = totalRI totalRE,
Restante = TotalHoras - totalRI - totalRE,
Index = index ,
GridHeight = altura
};
}
}
}
public DateTime GetPrimeiroDiaAno(int ano)
{
if (ano < Inicio.Year || ano > Fim.Year) throw new Exception("Ano Invalido");
else if (Inicio.Year == ano) return Inicio;
else return new DateTime(ano, 1, 1);
}
public DateTime GetUltimoDiaAno(int ano)
{
if (ano < Inicio.Year || ano > Fim.Year) throw new Exception("Ano Invalido");
else if (Fim.Year == ano) return Fim;
else return new DateTime(ano, 12, 31);
}
public int GetDuracaoDias(int ano)
{
if (ano < Inicio.Year || ano > Fim.Year) return 0;
else if (ano == Inicio.Year amp;amp; ano == Fim.Year) return (int)DateTimeUtils.CalculateBusinessDays(Inicio, Fim) 1;
else if (ano == Inicio.Year) return (int)DateTimeUtils.CalculateBusinessDays(Inicio, new DateTime(ano, 12, 31)) 1;
else if (ano == Fim.Year) return (int)DateTimeUtils.CalculateBusinessDays(new DateTime(ano, 1, 1), Fim) 1;
else return (int)DateTimeUtils.CalculateBusinessDays(new DateTime(ano, 1, 1), new DateTime(ano, 12, 31));
}
public double GetDuracaoMeses(int ano)
{
if (ano < Inicio.Year || ano > Fim.Year) return 0;
else if (ano == Inicio.Year amp;amp; ano == Fim.Year) return DateTimeUtils.CalculateMonths(Inicio, Fim);
else if (ano == Inicio.Year) return DateTimeUtils.CalculateMonths(Inicio, new DateTime(ano, 12, 31));
else if (ano == Fim.Year) return DateTimeUtils.CalculateMonths(new DateTime(ano, 1, 1), Fim);
else return 12;
}
}
public class AlocacaoPromotorTarefa
{
public double Actual { get; set; }
public double Restante { get; set; }
public int Index { get; set; }
public double GridHeight { get; set; }
}
}
Как вы можете видеть, в основном это геттеры, которые возвращают данные, которые мы хотим напрямую привязать к представлению, которые не являются частью модели. Эта практика делает разработку очень быстрой, поскольку написание конвертеров может быть довольно сложным для некоторых типов экспозиций.
Это большой объем кода, но, надеюсь, даст вам представление о том, что писать в ViewModel и что писать в модели, как расширить модель и как вы привязываете материал вокруг.
Комментарии:
1. Обертку модели я получил от Джоша Смита. Причина этого заключается в том, чтобы иметь возможность добавлять дополнительные функциональные возможности, которые в противном случае не принадлежат классу домена, или, что еще лучше, когда у вас нет никаких классов домена, как у меня. Например, реализовать интерфейс INotifyPropertyChanged в моем случае (без модели домена) непросто. Я думаю, вы могли бы сделать это, возможно, расширив класс entity, но я не знаю, как с этим работает. Но тогда также возникает вопрос о сохранении всего на своем собственном уровне. Я предполагаю, что View и его ViewModel — это уровень представления
2. Тогда у вас есть уровень сервиса, классы домена и DAL. Мой вопрос больше относится к M-MV-V-VM -> Model — ModelView, View — ViewModel, чем к MVVM. То есть для отсутствия класса сущности или ссылок на DAL на уровне представления, то есть для разделения задач. Я думал о своей ситуации и о том, как это можно было бы выполнить, и подумал, что ModelView (оболочка для сущности) может быть полезен при разделении.
3. Ну, имея уровень обслуживания, вы отделяете себя от DAL. Вы либо транспортируете сгенерированные объекты с помощью прокси-сервера, либо реальные объекты, в любом случае они являются вашей моделью на клиенте. Вы также можете расширить объекты, сгенерированные прокси-классом, для реализации измененного свойства INotifyProperty, однако это не очень хороший подход. Что вы действительно хотите, так это повторно использовать сущности на уровне DAL в вашем клиенте, что просто делается путем размещения проекта DAL в решении клиента и проверки того, что опция классы повторного использования отмечена при создании ссылки на веб-службу.