Статьи

Silverlight, MV-VM … и IoC — часть 4

Давайте перейдем к следующему шагу и удалим весь код в представлении, который непосредственно связан с командами, которые пользователь может давать с помощью кнопок. Для этого мы используем новую функцию Silverlight 4: Commanding.

После выпуска Silverlight 2 среда выполнения предложила класс — ICommand, — который мы могли бы использовать для реализации командной структуры. В WPF была полная реализация командного фреймворка, в то время как в Silverlight он был полностью предоставлен пользователю (пару решений можно найти в Интернете).

С выпуском Silverlight 4 каждый элемент управления, производный от ButtonBase, теперь предоставляет два новых свойства — Command и CommandParameter — вы можете использовать его в связывании с объектом ICommand, что позволяет применять декларативный подход при реагировании на события click, вызванные этими элементами управления; Теперь мы можем реализовать чистую форму MVVM, не полагаясь на внешние фреймворки.

Есть несколько хороших ресурсов об основах управления в Silverlight, вот два из них:

НОВАЯ КОМАНДНАЯ ПОДДЕРЖКА SILVERLIGHT 4

5 простых шагов к командованию в Silverlight

However we will follow a slightly different approach…ICommand is an interface, so it is well suited to be used with an IoC container :D. Defining our own set of commands — be it by registering them with names or by defining new interfaces that derive from ICommand and registering them — we can leave to the container the responsibility to configure the ViewModels with the actual instances of those commands that will be later on used with binding.

Это открывает множество возможностей: вы можете контролировать время жизни командного объекта через контейнер (временный или одноэлементный, позволяя вам совместно использовать поведение при необходимости), и вы можете «спамить» одну и ту же команду в разных разделах вашего интерфейса. легко (привязка к тому же свойству ViewModel); еще лучше, вы можете использовать методы AOP для добавления поведения к вашим командам (например, возможность ведения журнала или отслеживания, чтобы иметь некоторую статистику использования вашего приложения).

Давайте посмотрим на первый раз реализации; давайте определим базовый класс для наших команд:

public abstract class BaseCommand : ICommand
{
    bool canExecuteCache;

    protected abstract bool CanExecuteImpl(object parameter);

    #region ICommand Members

    public bool CanExecute(object parameter)
    {
        bool temp = CanExecuteImpl(parameter);

        if (canExecuteCache != temp)
        {
            canExecuteCache = temp;
            if (CanExecuteChanged != null)
            {
                CanExecuteChanged(this, new EventArgs());
            }
        }

        return canExecuteCache;
    }

    public event EventHandler CanExecuteChanged;

    public abstract void Execute(object parameter);
    
    #endregion
}

и (продолжение предыдущего поста в блоге) фактическая реализация команды поиска:

public class SearchCommand : BaseCommand
{
    public SearchCommand(ISearchViewModel viewModel)
    {
        _viewModel = viewModel;
    }

    private ISearchViewModel _viewModel;

    protected override bool CanExecuteImpl(object parameter)
    {
        return !_viewModel.IsBusy;
    }

    public override void Execute(object parameter)
    {
        _viewModel.PerformSearch(parameter.ToString());
    }
}

В этом простом примере я решил использовать функции, предоставляемые самим ViewModel, другой подход (возможно, для последующего рефакторинга) состоит в том, чтобы переместить возможности поиска из ViewModel в класс службы поиска и добавить это в команду и в ViewModel. , но давайте пока все упростим.

Теперь нам нужно изменить интерфейс и реализацию ViewModel:

public interface ISearchViewModel : IViewModel
{
    new ISearchView View { get; }

    /// <summary>
    /// Collection that will be used to expose the result of the search operation
    /// </summary>
    PagedCollectionView SearchResults { get; set; }

    /// <summary>
    /// performs an asyncronous search throught the webservice and returns the results filling in the SearchResults collection
    /// </summary>
    /// <param name="query"></param>
    void PerformSearch(string query);

    SearchCommand SearchCommand { get; set; }

    NavigateToAlbumCommand OpenAlbumCommand { get; set; }
}

Я опущу код для ViewModel, потому что мы просто добавили два свойства SearchCommand и OpenAlbumCommand для внедрения свойства (у нас не может быть таких в конструкторе, чтобы избежать циклических ссылок при разрешении объектов).

Теперь мы можем изменить код файла Wiew и стереть весь код, который использовался для подключения к событиям кнопки:

public partial class Search : Page, ISearchView
{
    protected Search()
    {
        InitializeComponent();

        Loaded += Search_Loaded;
    }

    public Search(ISearchViewModel viewModel)
        : this()
    {
        (this as IView).ViewModel = viewModel;
    }

    public ILogger Logger { get; set; }

    IViewModel IView.ViewModel { get { return DataContext as IViewModel; } set { DataContext = value; } }

    public ISearchViewModel ViewModel { get { return DataContext as ISearchViewModel; } }

    void Search_Loaded(object sender, RoutedEventArgs e)
    {
        // check if we asked for this search directly by navigating to this page
        // this should be maybe moved into the ViewModel 
        if (NavigationContext.QueryString.Count > 0)
        {
            object query = NavigationContext.QueryString["q"];
            if (query != null)
                ViewModel.PerformSearch(query.ToString());
        }
    }
}

Намного короче и чище, чем раньше, теперь нам нужно изменить XAML (и привязать к объектам ICommand) с:

...
<StackPanel Orientation="Horizontal" >
   <TextBox x:Name="TxtSearch" HorizontalAlignment="Left" Margin="0,8" Width="256" />
   <Button x:Name="BtnSearch" Margin="10,8" Width="100" Content="Search" Click="BtnSearch_Click"/>
   <Button x:Name="BtnAddNewAlbum" Margin="10,8" Width="100" Content="Add New Album" Click="BtnAddNewAlbum_Click"/>
</StackPanel>
...
<Border Tag="{Binding Id}" MouseLeftButtonDown="ItemClick" HorizontalAlignment="Center" VerticalAlignment="Center">
 	<Border.Effect>
   	<DropShadowEffect BlurRadius="100" ShadowDepth="0" Opacity="1" Color="#FFFFFF" />
  	</Border.Effect>
  	<Image Source="{Binding Image}" Width="60" Height="60" Cursor="Hand"/>
</Border>

Для того, чтобы:

...
<StackPanel Orientation="Horizontal" >
   <TextBox x:Name="TxtSearch" HorizontalAlignment="Left" />
   <Button x:Name="BtnSearch" Content="Search" Command="{Binding SearchCommand}" CommandParameter="{Binding ElementName=TxtSearch, Path=Text}" />
   <Button x:Name="BtnAddNewAlbum" Content="Add New Album" Command="{Binding OpenAlbumCommand}"/>
</StackPanel>
...
 <Button Command="{Binding ElementName=LayoutRoot, Path=DataContext.OpenAlbumCommand}" CommandParameter="{Binding Id}">
    <Button.Template>
        <ControlTemplate>
            <Border HorizontalAlignment="Center" VerticalAlignment="Center">
                <Border.Effect>
                    <DropShadowEffect BlurRadius="100" ShadowDepth="0" Opacity="1" Color="#FFFFFF" />
                </Border.Effect>
                <Image Source="{Binding Image}" Width="60" Height="60" Cursor="Hand"/>
            </Border>
        </ControlTemplate>
    </Button.Template>
</Button>
...

Мы удалили все события щелчка, и нам пришлось добавить кнопку вокруг изображения альбома для поддержки детализации на странице сведений (поскольку Border не наследуется от ButtonBase и, следовательно, не поддерживает командование).

Последний шаг — регистрация команд в контейнере IoC:

Container.Register(
   // search Feature
   Component.For<SearchCommand>(),
   Component.For<NavigateToAlbumCommand>(), 
   Component.For<ISearchViewModel>().ImplementedBy<SearchViewModel>().LifeStyle.Custom<Structura.Castle.Windsor.Lifecycle.SingletonLifestyleManager>(), 
   Component.For<ISearchView>().ImplementedBy<Search>().Named("Search")
);

В следующий раз, когда мы разрешим ISearchViewModel, он появится вместе со всеми настроенными командами.

Следуя этому шаблону, вы должны быть снова осторожны с зависимостями, которые есть у объекта: команда теперь имеет обязательную зависимость от ViewModel; Лучше, если вы не выполняете никаких действий над самим ViewModel в конструкторе команд, в основном потому, что вы еще не уверены, что ViewModel полностью создан (экземпляр команды генерируется при разрешении необязательных зависимостей ViewModel, так что, возможно, больше ссылки должны быть удовлетворены до того, как объект будет полностью настроен и готов к использованию). Это поведение отличается, если для реализации функции поиска вы используете внешний класс обслуживания вместо ViewModel.

Чтобы увидеть его в действии, обратитесь к моему тестовому проекту на CodePlex: http://dnmmusicstoresl.codeplex.com/ Changeset 42274