Подтвердить что ты не робот

Где хранить настройки/состояние приложения в приложении 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 является возможность тестирования моделей без приложения-хозяина. В частности, меня интересует понимание того, как централизованные настройки приложения влияют на тестируемость и возможность издеваться над зависимыми моделями.

4b9b3361

Ответ 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... но для меня слишком поздно быть рентабельным.