1 Comments

In the article How to prepare a cross platform visual studio project for MVVM, I explained how to create a Visual Studio solution for cross platform application development using PCL and MVVM. In this article I continue with the sample project and implement a very simple example to put things together and see how the project structure and organization is used.

Let’s do a quick example. Let’s do a list of persons and display it on each devices using our recently configured MVVM project.

The model

On the PCL project, on the “Models” folder, create a file called Person.cs and replace it with this code:

using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleApp.Models
{
    public class Person : ObservableObject
    {
        private string name;
        public string Name
        {
            get { return name; }
            set { Set(ref name, value); }
        }
    }
}

We are inheriting from ObservableObject (from MVVM Light) thus implementing INotifyPropertyChanged. We will now create a simple service to store data in the device. For this we need two services: a data service that will be common to all platforms, and a storage service that will be device specific.

The data service

Let’s start with the storage service. The interface for this service is declared in the PCL, but the implementation will be specific to both WinRT and WP. On the PCL project, on the “Services\Interfaces” folder, create a class called IStorageService.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleApp.Services
{
    public interface IStorageService
    {
        Task SaveTextAsync(string file, string text);

        Task<string> LoadTextAsync(string file);
    }
}

Now, let’s implement this interface on each of the devices. On the WinRT project, on the “Services” folder create a class named StorageService.cs:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;

namespace SampleApp.Services
{
    public class StorageService : IStorageService
    {

        public async Task SaveTextAsync(string fileName, string text)
        {
            StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);

            byte[] data = Encoding.UTF8.GetBytes(text);

            using (var s = await file.OpenStreamForWriteAsync())
            {
                await s.WriteAsync(data, 0, data.Length);
            }
        }

        public async Task<string> LoadTextAsync(string fileName)
        {
            try
            {
                StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(fileName);

                using (var s = await file.OpenStreamForReadAsync())
                {
                    using (var sr = new StreamReader(s))
                    {
                        return await sr.ReadToEndAsync();
                    }
                }
            }
            catch (Exception)
            {
                Debug.WriteLine("Error loading file '{0}'", fileName);
                return null;
            }
        }
    }
}

This is pretty simple, we just create a file on the application folder with the content. Let’s do the same for the WP project, but instead of creating and repeating the code, we will use the same file for the WinRT project, since the code is basically the same and it works without any changes on WP. This will be more and more the case as Microsoft is slowly converging both platforms, and now with the announce in Build2014 of universal apps this convergence is much closer. On the WP project, on the “Services” folder, add a link to the StorageService.cs file created before.

Now, we need to register both services with the IoC container. We will do this in LocatorService.cs file on the WinRT project (if you remember from the previous article, we just added this same file as a link on the WP project, se we need to change it once). Add the highlighted lines to the file:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Ioc;
using Microsoft.Practices.ServiceLocation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleApp.Services
{
    public class DeviceLocatorService
    {
        static DeviceLocatorService()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

            if (ViewModelBase.IsInDesignModeStatic)
            {
            }
            else
            {
            }

            if (!SimpleIoc.Default.IsRegistered<IStorageService>()) SimpleIoc.Default.Register<IStorageService, StorageService>();
        }

        public static void Cleanup()
        {
        }
    }
}

Compile your solution to make sure everything works. Now, let’s create the data service. This data service will use the storage service to store the data in an XML format. In the PCL project, on the “Services\Interfaces” folder, create a file called IDataService.cs and replace the code with this:

using SampleApp.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleApp.Services
{
    public interface IDataService
    {
        Task<IEnumerable<Person>> GetPersonsAsync();

        Task AddPersonAsync(Person person);
    }
}

As I mentioned before, let’s keep it simple :-). Now the implementation, in the PCL project, on the “Services” folder, create a file called DataService.cs with this code:

using SampleApp.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace SampleApp.Services
{
    public class DataService : IDataService
    {
        private const string dataFile = "data.xml";

        private IStorageService storageService;
        private List<Person> persons;

        public DataService(IStorageService storageService)
        {
            this.storageService = storageService;
        }

        public async Task<IEnumerable<Person>> GetPersonsAsync()
        {
            if (persons == null) await LoadAsync();
            return persons;
        }

        public async Task AddPersonAsync(Person person)
        {
            if (persons == null) await LoadAsync();
            persons.Add(person);
            await SaveAsync();
        }

        private async Task SaveAsync()
        {
            StringWriter sw = new StringWriter();
            XmlSerializer serializer = new XmlSerializer(typeof(List<Person>));
            serializer.Serialize(sw, persons);

            await storageService.SaveTextAsync(dataFile, sw.ToString());
        }

        private async Task LoadAsync()
        {
            string data = await storageService.LoadTextAsync(dataFile);

            if (!string.IsNullOrEmpty(data))
            {
                StringReader sr = new StringReader(data);
                XmlSerializer serializer = new XmlSerializer(typeof(List<Person>));

                persons = (List<Person>)serializer.Deserialize(sr);
                
            }
            else
            {
                persons = new List<Person>();
            }
        }
    }
}

The service just serializes and deserializes data to and from an XML file. Notice the constructor of the service, it expects as a parameter an instance to a storage service, this instance is resolved by the IoC container and injected at run time. The service that will be injected will be the one registered in the locator service specific to the platform where the app is running. Everything is wired up appropriately.

Design time data

Now, this works fine when running on the device, but what about Blend? What about design time data? Designers will thank us if we provide design time data as part of the project as well. Let’s do this. On the PCL project, create a folder called “Design” and add a class named DataService.cs:

using SampleApp.Models;
using SampleApp.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleApp.Design
{
    public class DataService : IDataService
    {
        public Task<IEnumerable<Person>> GetPersonsAsync()
        {
            List<Person> persons = new List<Person>() {
                new Person() { Name = "Giovanni" },
                new Person() { Name = "Antonio"}
            };

            TaskCompletionSource<IEnumerable<Person>> tcs = new TaskCompletionSource<IEnumerable<Person>>();
            tcs.SetResult(persons);
            return tcs.Task;
        }

        public Task AddPersonAsync(Person person)
        {
            throw new NotImplementedException();
        }
    }
}

The above class will provide design time data for our views. Notice that we only provide the implementation of the GetPersosnAsync method, since this is the only method that we will need at design time. Now, we need to register this service to be used instead of the previous one when we are working at design time. We do this in the LocatorService.cs file in the PCL project. Open the file and add the lines highlighted below:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Ioc;
using Microsoft.Practices.ServiceLocation;
using SampleApp.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SampleApp.Services
{
    public class LocatorService
    {
        static LocatorService()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

            // Services
            if (ViewModelBase.IsInDesignModeStatic)
            {
                SimpleIoc.Default.Register<IDataService, Design.DataService>();
            }
            else
            {
                SimpleIoc.Default.Register<IDataService, Services.DataService>();
            }

            // View Models
            SimpleIoc.Default.Register<IMainViewModel, MainViewModel>();
        }

        public IMainViewModel MainViewModel
        {
            get
            {
                return ServiceLocator.Current.GetInstance<IMainViewModel>();
            }
        }

        public static void Cleanup()
        {
        }
    }
}

With this the IoC container will inject the design time data service when in design time, and the real data service (the one that works with the device storage) at run time on the device.

We have completed the model and the data access, next thing is the view model.

The view model

Let’s add some methods to our view model. In the PCL project, on the “ViewModels\Interfaces” folder, edit the IMainViewModel.cs file as follows:

using GalaSoft.MvvmLight.Command;
using SampleApp.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace SampleApp.ViewModels
{
    public interface IMainViewModel
    {
        ObservableCollection<Person> Persons { get; }

        RelayCommand RefreshCommand { get; }
        RelayCommand AddCommand { get; }
    }
}
We’ve added an observable collection to bind to the views (we will do this later) and also commands to

execute business logic. Let’s implement the interface, open the MainViewModel.cs file and replace it with the following code:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using SampleApp.Models;
using SampleApp.Services;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace SampleApp.ViewModels
{
    public class MainViewModel : ViewModelBase,IMainViewModel
    {
        private IDataService dataService;

        public MainViewModel(IDataService dataService)
        {         
            this.dataService = dataService;

            RefreshAsync();

        }

        private ObservableCollection<Person> persons = new ObservableCollection<Person>();
        public ObservableCollection<Person> Persons
        {
            get
            {
                return persons;
            }
        }

        #region Commands

        #region Refresh
        private RelayCommand refreshCommand;
        public RelayCommand RefreshCommand
        {
            get
            {
                return refreshCommand
                    ?? (refreshCommand = new RelayCommand(
                                          async () =>
                                          {
                                              await RefreshAsync();
                                          }));
            }
        }

        private async Task RefreshAsync()
        {
            persons.Clear();
            foreach (Person person in await dataService.GetPersonsAsync())
                persons.Add(person);
        }
        #endregion

        #region Add
        private RelayCommand addCommand;
        public RelayCommand AddCommand
        {
            get
            {
                return addCommand
                    ?? (addCommand = new RelayCommand(
                                          async () =>
                                          {
                                              Person person = new Person() { Name = "Giovanni" };
                                              await dataService.AddPersonAsync(person);
                                              persons.Add(person);
                                          }));
            }
        }
        #endregion

        #endregion
    }
}

Notice how the constructor expects a reference to a data service, which, as explained before, will be injected by the IoC container. The other thing to notice is the use of the design time data, the constructor does a refresh of the data so we can have data when working with the views.

We have completed our view model. Now we will work the views.

The views

I will provide a very simple view, no beauty on it, we will leave this for the designer. Instead, let’s focus on the code. Let’s first work the WinRT view. We will add a grid view and an app bar to add and refresh the data. On the WinRT project, open the MainPage.xaml and replace it with the following:

<Page
    x:Class="SampleApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:SampleApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    DataContext="{Binding MainViewModel, Source={StaticResource Locator}}">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <GridView ItemsSource="{Binding Persons}">
            <GridView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <TextBlock Text="{Binding Name}" />
                    </Grid>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>
    </Grid>

    <Page.BottomAppBar>
        <AppBar>
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <AppBarButton Command="{Binding AddCommand}" Icon="AddFriend" Label="Add Person" />
                </StackPanel>

                <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                    <AppBarButton Command="{Binding RefreshCommand}" Icon="Refresh" Label="Refresh" />
                </StackPanel>
            </Grid>
        </AppBar>
    </Page.BottomAppBar>
</Page>

We have used data binding to bind the list of persons and the commands as well. No code-behind! If you do this in the VS XAML editor, you will see the design time data:

WinRT view

On WP, we have one problem: the app bar doesn’t provide binding to commands. For this we will rely on a cool package called AppBarUtils. On the WP project, add this package using the nuget package manager. Replace the code in MainPage.xaml with the following:

<phone:PhoneApplicationPage
    x:Class="SampleApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"    
    xmlns:abu="clr-namespace:AppBarUtils;assembly=AppBarUtils"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True"
    DataContext="{Binding MainViewModel,Source={StaticResource Locator}}">
    
    <!--LayoutRoot is the root grid where all page content is placed-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--TitlePanel contains the name of the application and page title-->
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>
            <TextBlock Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>

        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <phone:LongListSelector Margin="0,0,-22,0" ItemsSource="{Binding Persons}">
                <phone:LongListSelector.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <TextBlock Text="{Binding Name}" />
                        </Grid>
                    </DataTemplate>
                </phone:LongListSelector.ItemTemplate>
            </phone:LongListSelector>
        </Grid>

        <!--<Image Source="/Assets/AlignmentGrid.png" VerticalAlignment="Top" Height="800" Width="480" Margin="0,-32,0,0" Grid.Row="0" Grid.RowSpan="2" IsHitTestVisible="False" />-->
    </Grid>

    <phone:PhoneApplicationPage.ApplicationBar>
        <shell:ApplicationBar Mode="Default" Opacity="1.0" IsMenuEnabled="True" IsVisible="True">
            <shell:ApplicationBarIconButton IconUri="/Assets/AppBar/add.png" Text="add" />
            <shell:ApplicationBarIconButton IconUri="/Assets/AppBar/refresh.png" Text="refresh" />
        </shell:ApplicationBar>
    </phone:PhoneApplicationPage.ApplicationBar>

    <i:Interaction.Behaviors>
        <abu:AppBarItemCommand Id="add" Command="{Binding AddCommand}" Text="Add Person" />
        <abu:AppBarItemCommand Id="refresh" Command="{Binding RefreshCommand}" Text="Refresh" />
    </i:Interaction.Behaviors>
</phone:PhoneApplicationPage>

We have used data binding to bind the list of persons and the commands as well. Again, no code-behind! If you do this in the VS XAML editor, you will see the design time data:

WP

We have completed the views. Now, test it (pray) and make sure that everything works.

You can download the sample code from here:

Comments

Comment by Steven B

In your MVVM examples, you go through developing this for WP and W8 basically. I've made it this far on my own, developing the code to support Xamarin.Android(project not created yet) and a WPF Application. Coming down the line, most of the changes involved simply figuring out the syntax differences between this example and WPF, like the XAML.

Now that we're getting into creating files on this post, I'm having to do some things completely different due to libraries not being usable in WPF like specifically "Windows.Storage." Instead, I think "System.IO.IsolatedStorage" is the comparable library. The problem is, unlike StorageFile, IsolatedStorage has different objects to handle the same things (IsolatedStorageFile, IsolatedStorageFileStream, StreamReader, and StreamWriter).

How would you separate these objects to fit into your MVVM SampleApp?

Steven B