Страницы

Поиск по вопросам

среда, 25 декабря 2019 г.

Передача значения из View в Model и последующий вызов метода - как?

#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:

Комментариев нет:

Отправить комментарий