#c_sharp #net #wpf #xaml #инспекция_кода
Следуя паттерну MVVM имеем View, ViewModel и некие классы для Model, в частности ExcelImporter для импорта и парсинга экселевского файла. Во вьюхе есть поле ввода адреса файла. Биндится к соответствующему свойству в VM: public string ExcelPath { get { return excelPath; } set { excelPath = value; OnPropertyChanged("ExcelPath"); //? } } Вопрос 1: если значение ExcelPath меняется только из вьюхи пользователем через диалоговое окно выбора файла, то во ViewModel в сеттере ему ведь не нужно делать OnPropertyChanged("ExcelPath")? И, следовательно, нужно в биндинге оставить Mode=Default (он же OneWay)? Также в VM есть экземпляр класса ExcelImporter, который отвечает за импорт файла, со свойством, опять же, ExcelPath. Вопрос 2: где будет корректным передавать значение из VM.ExcelPath в ExcelImporter.ExcelPath? В сеттере VM.ExcelPath? Или ещё была шальная мысль сделать событие ExcelPathChanged, на которое подписать саму же VM, и в обработчике устанавливать ExcelImporter.ExcelPath = ExcelPath. Или подписать на это событие ExcelImporter, но это по MVVM вроде как совсем неправильно. public string ExcelPath { get { return excelPath; } set { excelPath = value; ExcelImporter.ExcelPath = ExcelPath; //? } } Вопрос 2.1: а можно вообще вот так сделать свойство в VM? public string ExcelPath { get { return ExcelImporter.ExcelPath; } set { ExcelImporter.ExcelPath = value; } } Вопрос 3: если в сеттере, то что именно присваивать: value, excelPath или, как в вопросе 2, ExcelPath? public string ExcelPath { get { return excelPath; } set { excelPath = value; ExcelImporter.ExcelPath = value; //? ExcelImporter.ExcelPath = excelPath; //? ExcelImporter.ExcelPath = ExcelPath; //? } } Вопрос 4: чтобы ExcelImporter сразу же после получения ExcelPath выполнял метод Import() - т.е. чтобы с точки зрения пользователя всё автоматически происходило после выбора файла из обычного OpenFileDialog, без всяких лишних нажатий кнопки типа "Импортировать" - этот метод должен вызываться где? В сеттере VM.ExcelPath, в сеттере ExcelImporter.ExcelPath или через событие в ExcelImporter, какой-нибудь ExcelPathChanged, на которое подписан... сам же ExcelImporter или VM, и уже там в обработчике вызывать ExcelImporter.Import()? Больше ничего в голову не приходит, а такое активное использование сеттера для кучи дополнительных действий вызывает сомнение. Вообще задача простая: получить из View адрес файла, передать его в ExcelImporter и вызвать метод Import(). Может, вообще все вопросы неправильные, и это делается как-то по-другому? И суть вопросов не в том как это сделать вообще, а как сделать правильно, религиозно верно, так сказать, а не мартышкиным методом "абы работало" :) PS Прошу прощения за некоторое нарушение правил SO, за несколько вопросов сразу, но как видите они взаимосвязаны, и создавать несколько отдельных тем показалось лишним. UPD1 @andrey-k Мне сложно было представить такую ситуацию, что импорт происходит сразу после ввода имени файла, минуя нажатие кнопки Ну поле, оно же TextBox, существует постольку-поскольку, чтобы была возможность скопипастить адрес файла, но в осноном конечно же выбор через винапишный OpenFileDialog. Хотя всё это сводится к одному - по сути что ввод ручками пользователя, что OpenFileDialog возвращает строкой путь к файлу. Разумеется, его нужно проверить. Но не вижу никакого смысла вынуждать пользователя тыкаться лишний раз ещё в какие-то кнопки после указания файла. Выбор документа и выуживание из него определённых данных - это первые 50% функционала.
Ответы
Ответ 1
По вопросу 1: View не должно знать, кто и как меняет свойства в VM. Завтра вы поменяете VM, и при этом вы не должны менять ещё и View. Пусть каждый из уровней заботится только о себе. По поводу OnPropertyChanged("ExcelPath"); //?: лучше писать, конечно, так: void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } и пользоваться просто NotifyPropertyChanged(): string excelPath; public string ExcelPath { get { return excelPath; } set { if (excelPath != value) { excelPath = value; NotifyPropertyChanged(); } } } По поводу того, когда обновлять модель — это решать вам и только вам. Может быть, обновлять модель сразу неправильно, а нужно подождать, пока пользователь скомандует «вот сейчас давайте». А может быть, нужно обновлять её как можно скорее. Вы как архитектор вашего приложения должны решать такие вопросы сами. В любом случае, обновление модели — дело VM, а не наоборот. По вопросу 2.1 — технически так делать можно, да (но не забудьте NotifyPropertyChanged()). Вопрос в том, правильно ли это для вашего случая. Например, если модель бежит не в главном потоке, то доступ к ней из UI-потока может быть неверным. По вопросу 3 — не имеет ровно никакого значения. Все три значат одно и то же. (Я всё же не использовал бы ExcelPath, чтобы не идти лишний раз через getter, но это вопрос личных предпочтений.) По поводу вопроса 4 — опять-таки это не диктуется паттерном MVVM. Всё определяется лично вами. Я бы делал так: при выставлении setter'а проверял значение на правильность, проверял, можно ли запускать импорт, и при этом условии запускал бы: string excelPath; public string ExcelPath { get { return excelPath; } set { if (excelPath == value) return; excelPath = value; NotifyPropertyChanged(); OnPathChanged(); } } async void OnPathChanged() { if (!IsValidPath(excelPath)) { SetInvalidFlag(); // so UI can pick it up return; } await BookNewImport(); } ExcelImporter currentImporter, pendingImporter; async Task BookNewImport() { var importer = new ExcelImporter(ExcelPath); if (currentImporter != null) { pendingImporter = importer; await currentImporter.CancelAsync(); if (pendingImporter != importer) // new import booked return; // so not run ours } currentImporter = importer; await currentImporter.ImportAsync(); if (currentImporter == importer) currentImporter = null; // else there's other importer running } Это лучше тем, что модель дёргается только после всех проверок. Плюс, поскольку импорт — процесс по сути длительный, я бы сделал к нему async-интерфейс, и не блокировал вызывающий поток (как это сделано в сниппете выше).Ответ 2
Не знаю как идеально, но могу сказать, как бы я сделал и почему. Я бы сделал так: private string _ExcelPath; public string ExcelPath { get { return _ExcelPath; } set { _ExcelPath = value; OnPropertyChanged("ExcelPath"); } } Делал свой пример не для диалога, а для поля, потому что прочитал Во вьюхе есть поле ввода адреса файла Сначала расскажу для случая с полем. То есть, когда пользователь вводит имя файла вручную. В своем примере предусмотрел развитие кода по следующем направлениям: Мне сложно было представить такую ситуацию, что импорт происходит сразу после ввода имени файла, минуя нажатие кнопки. Поэтому я на скорую руку сделал свою имплементацию ICommand. Во вью моделе определил эту команду, как свойство. Таким образом, вызов импортера не может произойти просто после ввода каких-то символов в поле, но происходит по вызову метода Execute() команды. При выполнении этого метода и считывается свойство ExcelPath. Сервис импорта не вызывается в сеттере в том числе и потому, что теоретически имя файла нужно бы сначала проверить, провести валидацию, а потом уже что-то с ним делать. Если подумать о пользовательском интерфейсе, то хорошо бы выводить адекватные сообщения об ошибках. В XAML, для поля ввода файла установлено UpdateSourceTrigger=PropertyChanged. После ввода каждого нового символа происходит обновления свойства вью модели, а так же изменяется состояние кнопки и индикатора. Если формат имени файла неверный, то кнопка серая и горит красный восклицательный знак. Красный восклицательный знак помимо серой кнопки введен для того случая, когда полей много, а кнопка одна, например, ОК. Тогда красный восклицательный знак показывает, какое именно поле косячное. Чтобы кнопка становилась серой, надо в сеттер добавить ImportExcelFileCommand.RaiseCanExecuteChanged(). Вдруг когда-нибудь станет нужным сброс этого поля изнутри вью-модели, поэтому в сеттере есть OnPropertyChanged("ExcelPath") и Mode=TwoWay. Теоретически, может быть и такая ошибка, которая не может быть обнаружена до нажатия кнопки. Напимер, существует ли файл. И так же нужно вывести адекватное сообщение. Если следивать принцмпам SOLID, то хорошо бы инъектировать сервис импорта в конструктор вью модели. И интуитивно мне кажется, что имя файла должно быть параметром функции: Import(fileName) Код: public partial class MainWindow : Window { private class ExcelImporter : IExcelImporter { public void Import(string excelFilePath) { MessageBox.Show(excelFilePath); } } public MainWindow() { DataContext = new ViewModel(new ExcelImporter()); InitializeComponent(); } } public class ViewModel: INotifyPropertyChanged { private readonly IExcelImporter _ExcelImporter; public MyQuickCommand ImportExcelFileCommand { get; private set; } //implements ICommand private string _ExcelPath; public string ExcelPath { get { return _ExcelPath; } set { _ExcelPath = value; OnPropertyChanged("ExcelPath"); ImportExcelFileCommand.RaiseCanExecuteChanged(); } } private bool _ExcelPathFormatIsValid; public bool ExcelPathFormatIsValid //if you want to indicate when the file format is incorrect { get { return _ExcelPathFormatIsValid; } set { _ExcelPathFormatIsValid = value; OnPropertyChanged("ExcelPathFormatIsValid"); } } public ViewModel(IExcelImporter excelImporter) { if (excelImporter == null) throw new ArgumentNullException("excelImporter"); _ExcelImporter = excelImporter; ImportExcelFileCommand = new MyQuickCommand(Import, IsExcelPathFormatValid); } private void Import() { if (!ExcelFileExists()) { //indicate error } else { try { _ExcelImporter.Import(_ExcelPath); } catch (Exception e) { throw e; } } } private bool IsExcelPathFormatValid() { bool validationResult; throw new NotImplementedException("ExcelPathIsValid"); ExcelPathFormatIsValid = validationResult; return validationResult; } private bool ExcelFileExists() { throw new NotImplementedException("FileExists"); } public event PropertyChangedEventHandler PropertyChanged = delegate { }; public void OnPropertyChanged(string propertyName) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } ICommand: public class MyQuickCommand : ICommand { private readonly Action _Execute; private readonly Func_CanExecute; public MyQuickCommand(Action executeAction) : this(executeAction, () => true) { } public MyQuickCommand(Action executeAction, Func canExecuteFunc) { if (executeAction == null) throw new ArgumentNullException("executeAction"); if (canExecuteFunc == null) canExecuteFunc = () => true; _Execute = executeAction; _CanExecute = canExecuteFunc; } public event EventHandler CanExecuteChanged = delegate { }; public bool CanExecute(object parameter) { if (parameter != null) throw new NotSupportedException("CanExecute MyCommand with parameter is not supported"); return _CanExecute(); } public void Execute(object parameter) { if (parameter != null) throw new NotSupportedException("Execute MyCommand with parameter is not supported"); _Execute(); } public void RaiseCanExecuteChanged() { CanExecuteChanged(this, EventArgs.Empty); } } public interface IExcelImporter { void Import(string excelFilePath); } XAML: Для случая с диалогом, ICommand остается. Диалог вызывается изнутри метода Execute(). Единственное, тогда свойство ExcelPath вообще не нужно, а приватные методы проверки будут принимать имя файла как параметр. Update: имел ввиду, что и поле TextBox тоже не нужно для копирования, а чтобы path получался напрямую из вью модели и из вью модели вызывался диалог через команду. Если отвечать конкретно на вопросы, то, с моей точки зрения: 1 - Mode=TwoWay. Update: это на случай если в будущем надо будет сбросить поле изнутри 2 - ICommand, как свойство вью-модели 2.1 - мне не нравится такая привязка напрямую. Во первых, может понадобиться валидация. Во вторых, мне кажется не очень красиво спроектирован сервис. Думаю, что должен быть метод importServiceInstance.Import(fileName). 3 - если имя файла устанавливается именно так, то я бы сделал ExcelImporter.ExcelPath = excelPath, потому что так нагляднее, чем =value, и не вызывает цепочек вызовов, как если =ExcelPath. 4 - ICommand. Хотя бы один раз нажать кнопку придется, чтобы открыть диалог. А оттуда из метода Execute() открывать диалог и потом вызывать импорт. P.S. Если надоедает каждый раз писать OnPropertyChanged("name"), то рекоммендую посмотреть Fody PropertyChanged
Комментариев нет:
Отправить комментарий