Следуя паттерну 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: 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-интерфейс, и не блокировал вызывающий поток (как это сделано в сниппете выше).