У меня обычно плохое предчувствие по поводу кода, в котором одна модель представления напрямую связана с другой. Мне нравится идея, что часть шаблона 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. На самом деле это не похоже на чью-либо ответственность, кроме SettingsProvider. Что приводит меня к мысли, что провайдер настроек должен прислушиваться к изменениям подключенного адреса и сохранять их без вмешательства. Другими словами, событие:
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 здесь, который я обсуждал в ответе на этот вопрос
Чтобы решить вопросы тестируемости, теперь у вас есть очень определенное ожидание того, что будет делать каждый метод. ConnectionViewModel вызовет соединение, ServiceProvider соединится, а SettingsProvider сохранится. Чтобы протестировать ConnectionViewModel, вы, вероятно, захотите преобразовать соединение с ServiceProvider из класса в интерфейс:
public class ServiceProvider : IServiceProvider
{
...
}
public class ConnectionViewModel
{
private IServiceProvider serviceProvider;
public ConnectionViewModel(IServiceProvider provider)
{
this.serviceProvider = serviceProvider;
}
...
}
Затем вы можете использовать фиктивную платформу для представления поддельного IServiceProvider, который вы можете проверить, чтобы убедиться, что метод connect был вызван с ожидаемыми параметрами.
Тестирование двух других классов является более сложной задачей, поскольку они будут полагаться на наличие реального сервера и реального постоянного устройства хранения. Вы можете добавить дополнительные уровни косвенности, чтобы отложить это (например, PersistenceProvider, который использует SettingsProvider), но в конечном итоге вы покидаете мир модульного тестирования и вступаете в интеграционное тестирование. Обычно, когда я кодирую с помощью шаблонов выше, модели и модели просмотра могут получить хорошее покрытие модульными тестами, но поставщики требуют более сложных методологий тестирования.
Конечно, если вы используете EventAggregator для разрыва связи и IOC для облегчения тестирования, то, вероятно, стоит взглянуть на одну из структур внедрения зависимостей, такую как Microsoft Prism, но даже если вы слишком опаздываете в разработке, чтобы повторно Разработчик многих правил и шаблонов может быть применен к существующему коду более простым способом.