다음을 통해 공유


Using the Repository pattern in Hilo (Windows Store apps using C++ and XAML)

From: Developing an end-to-end Windows Store app using C++ and XAML: Hilo

Previous page | Next page

For Hilo C++ we decided to use the Repository pattern to separate the logic that retrieves data from the Windows 8 file system from the presentation logic that acts on that data. We wanted to make the app easier to maintain and unit test which is why we selected the Repository pattern.

Download

After you download the code, see Getting started with Hilo for instructions.

You will learn

  • How to implement the Repository pattern for a Windows Store app using C++.
  • How to use the Repository pattern for unit testing your app.

Applies to

  • Windows Runtime for Windows 8
  • Visual C++ component extensions (C++/CX)
  • XAML

Introduction

The Repository pattern separates the logic that retrieves and persists data and maps it to the underlying model, from the business logic that acts upon that data. The repository is responsible for:

  • Mediating between the data source and the business layers of the app.
  • Querying the data source for data.
  • Mapping the data from the data source to the model.
  • Persisting changes in the model to the data source.

The separation of business logic from the data source offers these benefits:

  • Centralizing access to the underlying data source via a data access layer.
  • Isolating the data access layer to support unit testing.
  • Improving the code’s maintainability by separating business logic from data access logic.

For more info, see The Repository Pattern.

Hilo implements the Repository pattern by defining an abstract base class that has pure virtual member functions. These functions provide a base class from which other repository classes must inherit. A pure virtual member function is a member function that is defined as virtual and is assigned to 0. Such an abstract base class can’t be used to instantiate objects and serves only to act as an interface. Therefore, if a subclass of this abstract class needs to be instantiated, it has to implement each of the virtual functions, which results in the subclass supporting the interface defined by the abstract base class.

The Repository abstract base class defines a number of pure virtual member functions that must be overridden by member functions that have the same signatures in a derived class. The FileSystemRepository class derives from the Repository class and overrides the pure virtual member functions to query the file system for photos. A shared instance of the FileSystemRepository class is then used by the TileUpdateScheduler class and view model classes to read photos from the file system. The following illustration shows this relationship.

The StubRepository class also implements the pure virtual member functions defined in the Repository abstract base class, in order to provide a mock repository implementation for unit testing. An instance of the StubRepository class can then be passed into the view model classes from unit tests. Rather than return photos from the file system, this mock class simply returns mock photo objects. For more info about unit testing, see Testing the app.

The following illustration shows an overview of the repository architecture. The main advantage of this architecture is that a repository that accesses a data source, such as the file system, can easily be swapped out for a repository that accesses a different data source, such as the cloud.

In order to implement a new repository, which derives from the Repository abstract base class, you must also implement the IPhoto, IPhotoGroup, IYearGroup, and IMonthBlock interfaces. These interfaces are implemented in Hilo by the Photo, HubPhotoGroup, MonthGroup, YearGroup, and MonthBlock classes, respectively.

In order to explain how Hilo implements and uses the Repository class, we've provided a code walkthrough that demonstrates how the Rotate page retrieves a photo from the file system by using the Repository class.

[Top]

Code walkthrough

In Hilo, the App class contains a member variable, m_repository, of type Repository, which is instantiated as a shared pointer of type FileSystemRepository in the class constructor. This instance is created as shared pointer so that there’s only a single instance of the FileSystemRepository class in the application, which is passed between the required classes.

App.xaml.cpp

m_exceptionPolicy = ExceptionPolicyFactory::GetCurrentPolicy();
m_repository = std::make_shared<FileSystemRepository>(m_exceptionPolicy);

The OnLaunched method of the App class passes the shared pointer instance of the FileSystemRepository to the ScheduleUpdateAsync method of the TileUpdateScheduler class. The ScheduleUpdateAsync method uses this shared pointer instance to retrieve photos, from which thumbnails are generated for the app's live tile.

The App class exposes a singleton instance of the FileSystemRepository class through a GetRepository method, which is invoked by the ViewModelLocator class constructor to retrieve and store the shared pointer instance of the FileSystemRepository class.

ViewModelLocator.cpp

m_repository = safe_cast<Hilo::App^>(Windows::UI::Xaml::Application::Current)->GetRepository();

As both the ViewModelLocator and TileUpdateScheduler classes expect repositories of type shared_ptr<Repository>, any additional repository implementations that implement the Repository abstract base class can be used instead.

The ViewModelLocator class has properties that retrieve a view model object for each page of the app. For more info, see Using the MVVM pattern. The following code example shows the RotateImageVM property of the ViewModelLocator class.

ViewModelLocator.cpp

RotateImageViewModel^ ViewModelLocator::RotateImageVM::get()
{
    return ref new RotateImageViewModel(m_repository, m_exceptionPolicy);
}

The RotateImageVM property creates a new instance of the RotateImageViewModel class and passes in the shared pointer instance of the FileSystemRepository class, to the RotateImageViewModel constructor. The shared pointer instance of the FileSystemRepository class is then stored in the m_repository member variable of the RotateImageViewModel class, as shown in the following code example.

RotateImageViewModel.cpp

RotateImageViewModel::RotateImageViewModel(shared_ptr<Repository> repository, shared_ptr<ExceptionPolicy> exceptionPolicy) : 
    ImageBase(exceptionPolicy), m_repository(repository), m_imageMargin(Thickness(0.0)), m_getPhotoAsyncIsRunning(false),
    m_inProgress(false), m_isSaving(false), m_rotationAngle(0.0)

The Image control in the RotateImageView class binds to the Photo property of the RotateImageViewModel class. In turn, the Photo property invokes the GetImagePhotoAsync method to retrieve the photo for display, as shown in the following code example.

RotateImageViewModel.cpp

concurrency::task<IPhotoImage^> RotateImageViewModel::GetImagePhotoAsync()
{
    assert(IsMainThread());
    return m_repository->GetSinglePhotoAsync(m_photoPath);
}

GetImagePhotoAsync invokes GetSinglePhotoAsync on the shared pointer instance of the FileSystemRepository class that is stored in the m_repository member variable.

[Top]

Querying the file system

Hilo uses the FileSystemRepository class to query Pictures for photos. The view models use this class to provide photos for display.

For instance, the RotateImageViewModel class uses the GetImagePhotoAsync method to return a photo for display on the rotate page. This method, in turn, invokes the GetSinglePhotoAsync method in the FileSystemRepository. Here's the code.

FileSystemRepository.cpp

task<IPhotoImage^> FileSystemRepository::GetSinglePhotoAsync(String^ photoPath)
{
    assert(IsMainThread());
    String^ query = "System.ParsingPath:=\"" + photoPath + "\"";    
    auto fileQuery = CreateFileQuery(KnownFolders::PicturesLibrary, query, IndexerOption::DoNotUseIndexer);
    shared_ptr<ExceptionPolicy> policy = m_exceptionPolicy;
    return create_task(fileQuery->GetFilesAsync(0, 1)).then([policy](IVectorView<StorageFile^>^ files) -> IPhotoImage^
    {
        if (files->Size > 0)
        {
            IPhotoImage^ photo = (ref new Photo(files->GetAt(0), ref new NullPhotoGroup(), policy))->GetPhotoImage();
            create_task(photo->InitializeAsync());
            return photo;
        }
        else
        {
            return nullptr;
        }
    }, task_continuation_context::use_current());
}

The GetSinglePhotoAsync method calls CreateFileQuery to create a file query that will retrieve information about the file system using Advanced Query Syntax. GetFilesAsync is then called to execute the query, and the resulting async operation returns an IVectorView collection of StorageFile references. Each StorageFile object represents a file in the file system that matches the query. However, in this case the IVectorView collection will contain a maximum of one StorageFile object, because GetFilesAsync only returns one file that matches the query. The value-based continuation, which runs on the main thread, then creates and initializes an instance of the PhotoImage class to represent the single file returned by GetFilesAsync. The PhotoImage instance is then returned from GetSinglePhotoAsync to GetImagePhotoAsync in the RotateImageViewModel class.

[Top]

Detecting file system changes

Some of the pages in the app respond to file system changes in Pictures while the app is running. The hub page, image browser page, and image view page refresh after detecting an underlying file system change to Pictures. The hub page and image browser page refresh no more than one time every 30 seconds, even if file change notifications arrive faster than that. This is to avoid pages being updated too many times in response to images being added to Pictures while the app is running.

To implement this, the HubPhotoGroup, ImageBrowserViewModel, and ImageViewModel classes each create a function object in their constructors that will be invoked when photos are added, deleted, or otherwise modified in the folders that have been queried. The function object is then passed into the AddObserver method of the FileSystemRepository class. Here’s the code.

ImageViewModel.cpp

auto wr = WeakReference(this);
function<void()> callback = [wr] {
    auto vm = wr.Resolve<ImageViewModel>();
    if (nullptr != vm)
    {
        vm->OnDataChanged();
    }
};
m_repository->AddObserver(callback, PageType::Image);

This code allows the OnDataChanged method in the ImageViewModel to be invoked if the underlying file system changes while the app is running. When the OnDataChanged method is invoked, it re-queries the file system to update the photo thumbnails on the filmstrip. The AddObserver method, in the FileSystemRepository class, simply stores the function object in a member variable for later use.

When the QueryPhotosAsync method in the ImageViewModel class is invoked to get the photos for display on the filmstrip, the GetPhotosForDateRangeQueryAsync method will be invoked in the FileSystemRepository class. This method queries Pictures for photos that fall within a specific date range, and returns any matching photos. After the query is created, a QueryChange object will be created to register a function object to be invoked if the results of the file system query changes. Here’s the code.

FileSystemRepository.cpp

m_allPhotosQueryChange = (m_imageViewModelCallback != nullptr) ? ref new QueryChange(fileQuery, m_imageViewModelCallback) : nullptr;

The QueryChange class is used to monitor the query for file system changes. The constructor accepts a file query and a function object. It then registers a handler for the ContentsChanged event that fires when an item is added, deleted or modified in the folder being queried. When this event fires, the function object passed into the class is executed.

The overall effect of this is that the image view page displays a collection of photo thumbnails on the filmstrip that fall within a specific date range and the full photo for the selected thumbnail. If photos from that date range are added to the file system, deleted from the file system, or otherwise modified, the app will be notified that the relevant file system content has changed and the thumbnails will be refreshed.

Note  The handler for this event is still registered when the app is suspended. However, the app won't receive ContentsChanged events while it is suspended. Instead, when the app resumes, it refreshes the current view.

 

When the app resumes from a suspended state, the OnResume method in the App class invokes the NotifyAllObservers method in the FileSystemRepository class, which in turn invokes the function objects for the hub, image browser, and image view pages so that the relevant content is refreshed.

[Top]