Thursday, 29 November 2012

Creating an attached behaviour in a C++/CX app

Previously I’ve developed a sample photo viewing app to show how to implement a Windows Store app in C++/CX. The app uses MVVM as an architectural pattern in order to separate concerns between the user interface controls and their logic.

In MVVM, ideally the views should be defined purely in XAML, with a limited code-behind that does not contain business logic. However, in the sample app there is an event handler in the code behind for the MainView. The XAML file for the MainView class contains a GridView that is used to display photo thumbnails, and which registers an event handler for the ItemClick event.

<GridView Grid.Row="1"
          IsItemClickEnabled="True"
          ItemClick="OnPhotoClick"
          ItemsSource="{Binding Photos}"
          Margin="140,0,0,0"
          SelectionMode="Single">

When a GridViewItem (which represents a photo thumbnail) is clicked upon the ItemClick event of the GridView fires, which runs the OnPhotoClick event handler in the MainView code-behind. This event handler is used to navigate the app to the Photo page, which displays the full photo rather than just it’s thumbnail.

void MainView::OnPhotoClick(Object^ sender, ItemClickEventArgs^ e)
{
    auto photo = dynamic_cast<Windows::Storage::BulkAccess::FileInformation^>(e->ClickedItem);
    if (nullptr != photo)
    {
        PhotoNavigationData photoData(photo);
        PhotoViewerPage::NavigateToPage(PageType::Photo, photoData.SerializeToString());
    }
}

This approach presents a number of disadvantages:

  1. There’s event handler code in the code-behind file. For MVVM purists this is unacceptable as it doesn’t promote a clean separation of concerns.
  2. Navigation is being invoked from a view. Ideally, navigation should be invoked from a view model, from where it can be easily tested.

These disadvantages can be addressed by implementing an attached behaviour that executes a view model command in response to the ItemClick event of the GridView being raised.

Implementation

This implementation defines a behaviour called ListViewItemClickBehaviour. This behaviour executes a view model command in response to the ItemClick event firing on the GridView. The first step in the implementation is to define a custom attached property for the behaviour. The syntax of a custom attached property can be viewed at Custom attached properties.

An attached property is a specialized form of a dependency property. Attaching a behaviour to an object simply means making the object do something that it would not normally do. The idea is that you set an attached property on an element so that you can gain access to the element from the class that exposes the attached property. Once that class has access to the element it can hook onto events on it, and in response to those events firing make the element do things that it would normally not do.

There are several options for how to split the implementation between the header and the code file. The usual split is to declare the DependencyProperty as a public static property in the header, with a get implementation but no set. The get implementation refers to a private field, which is an uninitialized DependencyProperty instance. You should also declare the wrappers in the header file.

static property Windows::UI::Xaml::DependencyProperty^ CommandProperty
{
    Windows::UI::Xaml::DependencyProperty^ get();
}
static DelegateCommand^ GetCommand(Windows::UI::Xaml::DependencyObject^ element);
static void SetCommand(Windows::UI::Xaml::DependencyObject^ element, DelegateCommand^ value);

The DependencyProperty should be initialized in the code file outside of any function definition, via a call to the RegisterAttached method. The return value of the RegisterAttached method is used to fill the static but uninitialized identifier that is declared in the header file. In C++ the property is registered the first time the owner class is used. The reason why there’s a private backing field plus a public read-only property that surfaces the DependencyProperty is so that the other callers who use your dependency property can also use property-system utility APIs that require the identifier. If you keep the identifier private, people can’t use these utility APIs. For more information see Custom dependency properties.

DependencyProperty^ ListViewItemClickBehaviour::_commandProperty =
    DependencyProperty::RegisterAttached("Command",
    DelegateCommand::typeid, ListViewItemClickBehaviour::typeid,
    ref new PropertyMetadata(nullptr, ref new PropertyChangedCallback(&ListViewItemClickBehaviour::OnCommandPropertyChanged)));
 
DependencyProperty^ ListViewItemClickBehaviour::CommandProperty::get() 
{
    return _commandProperty;
}
 
DelegateCommand^ ListViewItemClickBehaviour::GetCommand(DependencyObject^ element)
{
    return safe_cast<DelegateCommand^>(element->GetValue(ListViewItemClickBehaviour::CommandProperty));
}
 
void ListViewItemClickBehaviour::SetCommand(Windows::UI::Xaml::DependencyObject^ element, DelegateCommand^ value)
{
    element->SetValue(ListViewItemClickBehaviour::CommandProperty, value);
}

Note that property metadata is assigned to the DependencyProperty. Two behaviours are specified – a default value that the property system assigns to all cases of the property, and a static callback method that is automatically invoked within the property system whenever a property value is detected. Therefore, the idea is that when the value of the Command DependencyProperty changes, the OnCommandPropertyChanged callback specified in the DependencyProperty will be invoked.

void ListViewItemClickBehaviour::OnCommandPropertyChanged(DependencyObject^ sender, DependencyPropertyChangedEventArgs^ e) 
{
    ListViewBase^ listView = safe_cast<ListViewBase^>(sender);
    if (listView != nullptr)
    {
        _itemClickEventToken = listView->ItemClick::add(ref new ItemClickEventHandler(&ListViewItemClickBehaviour::OnListViewItemClicked));
        _unloadedEventToken = listView->Unloaded::add(ref new RoutedEventHandler(&ListViewItemClickBehaviour::OnListViewUnloaded));
    }
}
 
void ListViewItemClickBehaviour::OnListViewItemClicked(Object^ sender, ItemClickEventArgs^ e)
{
    ListViewBase^ listView = safe_cast<ListViewBase^>(sender);
 
    auto command = GetCommand(listView);
    if (command != nullptr)
    {
        command->Execute(e->ClickedItem);
    }
}
 
void ListViewItemClickBehaviour::OnListViewUnloaded(Object^ sender, RoutedEventArgs^ e)
{
    ListViewBase^ listView = safe_cast<ListViewBase^>(sender);
    listView->ItemClick::remove(_itemClickEventToken);
    listView->Unloaded::remove(_unloadedEventToken);
}

The OnCommandPropertyChanged method gets the instance of ListViewBase and registers event handlers for the ItemClick and Unloaded events. The ListViewBase class is the base class for both the GridView and the ListView. The OnListViewItemClicked method handles the ItemClick event for the ListViewBase instance and gets the command associated with the instance and executes it. The OnListViewUnloaded method handles the Unloaded event and simply deregisters the event handlers when the control is unloaded (such as when page navigation occurs).

In the MainViewModel class, the constructor creates a DelegateCommand instance for the NavigateCommand property that will execute the NavigateToPhotoPage method when the command is executed.

MainViewModel::MainViewModel(shared_ptr<Repository> repository) : m_repository(repository)
{
     m_navigateCommand = ref new DelegateCommand(ref new ExecuteDelegate(this, &MainViewModel::NavigateToPhotoPage), nullptr);
}
 
ICommand^ MainViewModel::NavigateCommand::get()
{
    return m_navigateCommand;
}
 
void MainViewModel::NavigateToPhotoPage(Object^ parameter)
{
    auto photo = dynamic_cast<Windows::Storage::BulkAccess::FileInformation^>(parameter);
    if (nullptr != photo)
    {
        PhotoNavigationData photoData(photo);
        GoToPage(PageType::Photo, photoData.SerializeToString());
    }
}

The NavigateToPhotoPage method replicates the code that previously was present in the OnPhotoClick event handler in the MainView page. It uses the GoToPage method contained in the base ViewModel class.

Finally, the behaviour must be attached to the GridView in the MainView class. To do this the attach property syntax is used.

<GridView local:ListViewItemClickBehaviour.Command="{Binding NavigateCommand}"
          Grid.Row="1"
          IsItemClickEnabled="True"
          ItemsSource="{Binding Photos}"
          Margin="140,0,0,0"
          SelectionMode="Single">

Now the GridView does not specify an event handler for the ItemClick method, instead specifying a command that will be executed for the attached behaviour. When the value of the ListViewItemClickBehaviour::Command DependencyProperty is initialized to the NavigateCommand in the MainViewModel class, the ListViewItemClickBehaviour::OnCommandPropertyChanged event handler executes, which registers event handlers for the ItemClick and Unloaded events of the GridView. So when the user clicks on a photo thumbnail, the ItemClick event of the GridView fires, which executes the ListViewItemClickBehaviour::OnListViewItemClicked event handler, which executes the NavigateCommand in the MainViewModel class. The NavigateCommand executes the MainViewModel::NavigateToPhotoPage method, which executes the ViewModel::GoToPage method, causing page navigation to occur. Just before page navigation occurs, the Unloaded event on the GridView fires, causing the ListViewItemClickBehaviour:: OnListViewUnloaded event handler to run, which de-registers the ItemClick and Unloaded event handlers in order to prevent memory leaks.

Summary

The attached behaviour implemented here allows a view model command to be executed when an event fires on a GridView control. This approach eliminates the event handler that was present in the MainViewModel class, and ensures that navigation is only invoked from view model classes, where it can be easily tested. A behaviour could be implemented for other common events such as Tapped, by working on a UIElement or a Control. Such a behaviour would then work for all derived controls.

The sample app can be downloaded here.

No comments:

Post a Comment