Где хранить настройки/состояние приложения в приложении MVVM
Я экспериментирую с MVVM в первый раз и очень люблю разделение обязанностей. Конечно, любой шаблон дизайна решает многие проблемы - не все. Поэтому я пытаюсь выяснить, где хранить состояние приложения и где хранить приложения с широкими командами.
Допустим, мое приложение подключается к определенному URL-адресу. У меня есть ConnectionWindow и ConnectionViewModel, которые поддерживают сбор этой информации от пользователя и вызывают команды для подключения к этому адресу. В следующий раз, когда приложение запустится, я хочу снова подключиться к этому же адресу, не запрашивая пользователя.
Мое решение до сих пор заключается в создании ApplicationViewModel, который предоставляет команду для подключения к определенному адресу и сохранения этого адреса в каком-то постоянном хранилище (там, где он фактически был сохранен, не имеет значения для этого вопроса). Ниже приведена сокращенная модель класса.
Модель представления приложения:
public class ApplicationViewModel : INotifyPropertyChanged
{
public Uri Address{ get; set; }
public void ConnectTo( Uri address )
{
// Connect to the address
// Save the addres in persistent storage for later re-use
Address = address;
}
...
}
Модель вида подключения:
public class ConnectionViewModel : INotifyPropertyChanged
{
private ApplicationViewModel _appModel;
public ConnectionViewModel( ApplicationViewModel model )
{
_appModel = model;
}
public ICommand ConnectCmd
{
get
{
if( _connectCmd == null )
{
_connectCmd = new LambdaCommand(
p => _appModel.ConnectTo( Address ),
p => Address != null
);
}
return _connectCmd;
}
}
public Uri Address{ get; set; }
...
}
Итак, вопрос заключается в следующем: Является ли ApplicationViewModel правильным способом справиться с этим? Как еще вы можете сохранить состояние приложения?
EDIT: Я хотел бы также знать, как это влияет на тестируемость. Одной из основных причин использования MVVM является возможность тестирования моделей без приложения-хозяина. В частности, меня интересует понимание того, как централизованные настройки приложения влияют на тестируемость и возможность издеваться над зависимыми моделями.
Ответы
Ответ 1
Если вы не использовали M-V-VM, решение прост: вы помещаете эти данные и функциональные возможности в свой производный тип приложения. Затем Application.Current дает вам доступ к нему. Проблема, как вам известно, заключается в том, что Application.Current вызывает проблемы при модульном тестировании ViewModel. Это необходимо исправить. Первый шаг - отделить себя от конкретного примера приложения. Сделайте это, определив интерфейс и внедряя его в свой конкретный тип приложения.
public interface IApplication
{
Uri Address{ get; set; }
void ConnectTo(Uri address);
}
public class App : Application, IApplication
{
// code removed for brevity
}
Теперь следующий шаг - исключить вызов Application.Current в ViewModel с помощью функции "Инверсия управления" или "Локатор сервисов".
public class ConnectionViewModel : INotifyPropertyChanged
{
public ConnectionViewModel(IApplication application)
{
//...
}
//...
}
Все "глобальные" функции теперь предоставляются через макетный интерфейс службы IApplication. Вам по-прежнему остается вопрос о том, как создать ViewModel с правильным экземпляром службы, но похоже, что вы уже справляетесь с этим? Если вы ищете решение там, Onyx (отказ от ответственности, я автор) может предоставить решение там. Ваше приложение будет подписано на событие View.Created и добавит себя в качестве службы, и структура будет работать с остальными.
Ответ 2
Я вообще плохо отношусь к коду, у которого есть одна модель представления, напрямую общающаяся с другим. Мне нравится идея, что часть шаблона VVM должна быть в основном подключаемой, и ничто внутри этой области кода не должно зависеть от существования чего-либо еще в этом разделе. Причиной этого является то, что без централизации логики становится трудно определить ответственность.
С другой стороны, на основе вашего фактического кода может быть просто, что ApplicationViewModel плохо назван, он не делает модель доступной для представления, поэтому это может быть просто неправильный выбор имени.
В любом случае решение сводится к разрыву ответственности. Как я вижу, у вас есть три вещи, которые нужно достичь:
- Разрешить пользователю запрашивать подключение к адресу
- Используйте этот адрес для подключения к серверу
- Сохраняйте этот адрес.
Я бы предположил, что вам нужны три класса вместо двух.
public class ServiceProvider
{
public void Connect(Uri address)
{
//connect to the server
}
}
public class SettingsProvider
{
public void SaveAddress(Uri address)
{
//Persist address
}
public Uri LoadAddress()
{
//Get address from storage
}
}
public class ConnectionViewModel
{
private ServiceProvider serviceProvider;
public ConnectionViewModel(ServiceProvider provider)
{
this.serviceProvider = serviceProvider;
}
public void ExecuteConnectCommand()
{
serviceProvider.Connect(Address);
}
}
Следующее, что нужно решить, - это то, как адрес попадает на SettingsProvider. Вы можете передать его из ConnectionViewModel, как и в настоящее время, но я не заинтересован в этом, потому что он увеличивает сцепление модели представления, и это не является обязанностью ViewModel знать, что она нуждается в сохранении. Другой вариант - сделать звонок с ServiceProvider, но мне это не очень нравится, как и обязанность ServiceProvider. На самом деле это не похоже на любую ответственность, кроме настройкиProvider. Это заставляет меня полагать, что провайдер настроек должен выслушивать изменения в связанном адресе и продолжать их без вмешательства. Другими словами, событие:
public class ServiceProvider
{
public event EventHandler<ConnectedEventArgs> Connected;
public void Connect(Uri address)
{
//connect to the server
if (Connected != null)
{
Connected(this, new ConnectedEventArgs(address));
}
}
}
public class SettingsProvider
{
public SettingsProvider(ServiceProvider serviceProvider)
{
serviceProvider.Connected += serviceProvider_Connected;
}
protected virtual void serviceProvider_Connected(object sender, ConnectedEventArgs e)
{
SaveAddress(e.Address);
}
public void SaveAddress(Uri address)
{
//Persist address
}
public Uri LoadAddress()
{
//Get address from storage
}
}
Это вводит плотную связь между ServiceProvider и SettingsProvider, которые вы хотите избежать, если это возможно, и я бы использовал здесь EventAggregator, о чем я говорил в ответ на this вопрос
Чтобы устранить проблемы, связанные с тестируемостью, теперь у вас есть очень определенное ожидание того, что будет делать каждый метод. ConnectionViewModel вызовет соединение, ServiceProvider будет подключаться, и ConfigurationProvider будет сохраняться. Чтобы проверить ConnectionViewModel, вы, вероятно, захотите преобразовать связь с ServiceProvider из класса в интерфейс:
public class ServiceProvider : IServiceProvider
{
...
}
public class ConnectionViewModel
{
private IServiceProvider serviceProvider;
public ConnectionViewModel(IServiceProvider provider)
{
this.serviceProvider = serviceProvider;
}
...
}
Затем вы можете использовать фальшивую фреймворк, чтобы ввести насмешливый IServiceProvider, который вы можете проверить, чтобы убедиться, что метод connect был вызван с ожидаемыми параметрами.
Тестирование двух других классов является более сложным, поскольку они будут полагаться на наличие реального сервера и реального постоянного устройства хранения. Вы можете добавить больше уровней косвенности, чтобы отложить это (например, PersistenceProvider, который использует SettingsProvider), но в конечном итоге вы покинете мир модульного тестирования и введите интеграционное тестирование. Обычно, когда я кодирую шаблоны выше моделей и моделей просмотра, можно получить хорошее покрытие unit test, но поставщики требуют более сложных методологий тестирования.
Конечно, как только вы используете EventAggregator для разрыва связи и IOC для облегчения тестирования, вероятно, стоит посмотреть в одну из инфраструктур инъекций зависимостей, таких как Microsoft Prism, но даже если вы слишком поздно в разработке, архитектор, многие правила и шаблоны могут быть применены к существующему коду более простым способом.
Ответ 3
Да, вы на правильном пути. Когда у вас есть два элемента управления в вашей системе, которым необходимо обмениваться данными, вы хотите сделать это таким образом, чтобы он был как можно более развязан. Существует несколько способов сделать это.
В Prism 2 у них есть область, которая похожа на "шину данных". Один элемент управления может создавать данные с ключом, который добавляется к шине, и любой элемент управления, который хочет, чтобы данные могли регистрировать обратный вызов, когда эти данные изменяются.
Лично я реализовал то, что я называю "ApplicationState". Это имеет ту же цель. Он реализует INotifyPropertyChanged, и любой человек в системе может писать конкретные свойства или подписаться на события изменений. Он менее общий, чем решение Prism, но он работает. Это в значительной степени то, что вы создали.
Но теперь у вас есть проблема о том, как передать состояние приложения. Старая школа способ сделать это - сделать его Синглтон. Я не большой поклонник этого. Вместо этого у меня есть интерфейс, определяемый как:
public interface IApplicationStateConsumer
{
public void ConsumeApplicationState(ApplicationState appState);
}
Любой визуальный компонент в дереве может реализовать этот интерфейс и просто передать состояние приложения в ViewModel.
Затем в корневом окне, когда запускается событие Loaded, я просматриваю визуальное дерево и просматриваю элементы управления, которым требуется состояние приложения (IApplicationStateConsumer). Я передаю им appState, и моя система инициализируется. Это инъекция зависимости бедных людей.
С другой стороны, Призма решает все эти проблемы. Я хотел бы, чтобы я мог вернуться и перепроектировать с помощью Prism... но для меня слишком поздно быть рентабельным.