Где хранить настройки / состояние приложения в приложении MVVM - PullRequest
25 голосов
/ 24 апреля 2009

Я экспериментирую с 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 правильным способом для этого? Как еще вы можете хранить состояние приложения?

РЕДАКТИРОВАТЬ: Я хотел бы знать также, как это влияет на тестируемость. Одной из основных причин использования MVVM является возможность тестирования моделей без хост-приложения. В частности, меня интересует понимание того, как централизованные настройки приложения влияют на тестируемость, и возможность макетировать зависимые модели.

Ответы [ 3 ]

11 голосов
/ 29 апреля 2009

У меня обычно плохое предчувствие по поводу кода, в котором одна модель представления напрямую связана с другой. Мне нравится идея, что часть шаблона VVM должна быть в основном подключаемой, и ничто внутри этой области кода не должно зависеть от существования чего-либо еще в этом разделе. Причиной этого является то, что без централизации логики может быть сложно определить ответственность.

С другой стороны, исходя из вашего реального кода, может случиться так, что ApplicationViewModel имеет неправильное имя, это не делает модель доступной для представления, поэтому это может быть просто плохой выбор имени.

В любом случае, решение сводится к снятию ответственности. С моей точки зрения, вам нужно достичь трех целей:

  1. Разрешить пользователю запрашивать подключение к адресу
  2. Используйте этот адрес для подключения к серверу
  3. Сохраните этот адрес.

Я бы посоветовал вам использовать три класса вместо двух.

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, но даже если вы слишком опаздываете в разработке, чтобы повторно Разработчик многих правил и шаблонов может быть применен к существующему коду более простым способом.

9 голосов
/ 28 апреля 2009

Если вы не использовали 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 голосов
/ 24 апреля 2009

Да, вы на правильном пути. Если у вас в системе есть два элемента управления, которым необходимо передавать данные, вы хотите сделать это максимально разъединенным образом. Есть несколько способов сделать это.

В Prism 2 они имеют область, похожую на «шину данных». Один элемент управления может генерировать данные с ключом, который добавляется к шине, и любой элемент управления, которому нужны эти данные, может зарегистрировать обратный вызов при изменении этих данных.

Лично я реализовал то, что я называю "ApplicationState". Это имеет ту же цель. Он реализует INotifyPropertyChanged, и любой в системе может писать в определенные свойства или подписываться на события изменения. Он менее универсален, чем решение Prism, но работает. Это в значительной степени то, что вы создали.

Но теперь у вас есть проблема, как обойти состояние приложения. Старый способ сделать это - сделать его синглтоном. Я не большой поклонник этого. Вместо этого у меня есть интерфейс, определенный как:

public interface IApplicationStateConsumer
{
    public void ConsumeApplicationState(ApplicationState appState);
}

Любой визуальный компонент в дереве может реализовать этот интерфейс и просто передать состояние приложения в ViewModel.

Затем в корневом окне при возникновении события Loaded я пересекаю визуальное дерево и ищу элементы управления, которым требуется состояние приложения (IApplicationStateConsumer). Я передаю им appState, и моя система инициализируется. Это инъекция зависимости бедного человека.

С другой стороны, Prism решает все эти проблемы. Я бы хотел вернуться и перестроить архитектуру с использованием Prism ... но для меня уже слишком поздно быть экономически эффективным.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...