Miroslav Holec

Software & Cloud Architect

miroslavholec.cz / blog / windows-phone-isolated-storage-api-service

Windows Phone + Isolated Storage + Api service

Miroslav Holec

Publikován 14. října 2014 , aktualizace: 29. března 2016 |

Tento článek je starší 18 měsíců a je proto možné, že popisuje postupy nebo technologie, které v uplynulé době mohly doznat výraznějších změn. Názory a myšlenky v tomto článku již nemusí vyjadřovat současné stanovisko autora nebo autorů. Článek byl napsán 14. října 2014.

V tomto článku popíšu osvědčený pattern, který používám při návrhu Windows Phone aplikací. V třídách ListOfArticles a Article je použita implementace ObservableObject z MVVM Light Toolkitu.

Obsah článku

  • MainViewModel.cs - hlavní ViewModel, který získává data z ApiService při prvním volání a refreshi
  • ApiService.cs - služba, která komunikuje se vzdáleným API, jejím úkolem je stáhnout data a ukládat je průběžně do storage
  • StorageService.cs - služba, která umožňuje ukládání a načítání dat z IsolatedStorage
  • ListOfArticles.cs - kolekce článků
  • Article.cs - jeden konkrétní článek

MainViewModel.cs

Tento ViewModel využívá dvě services. Tou podstatnou je ApiService, která poskytuje data ze vzdáleného aplikačního rozhraní. Metoda GetArticles() vrací i stav operace, na základě kterého je možné rozhodnout o chování GUI.

public class MainViewModel : ViewModelBase
{
	private readonly IApiService apiService;
	private readonly IDialogService dialogService;

	public ObservableCollection<BaseArticle> Articles { get; private set; }

    public MainViewModel(IApiService apiService, IDialogService dialogService)
    {
        this.apiService = apiService;
		this.dialogService = dialogService;

        Articles = new ObservableCollection<Article>();

        RefreshViewModel();
    }

	private void RefreshViewModel()
   	{
        apiService.GetArticles((articles, state, error) =>
            {
                switch (state)
                {
                    case ResultState.ConnectivityFailed:
                        dialogService.ShowMessage(AppResources.ConnectivityFailed);
                        return;

                    case ResultState.JsonDeserialisationFailed:
                        dialogService.ShowMessage(AppResources.JsonDeserialisationFailed);
                        return;
                }

                Articles.Clear();
                articles.Items.ForEach(Articles.Add);

            });
	}
}

ApiService.cs

ApiService je zodpovědná za vracení dat získaných ze vzdáleného API. Data nevrací přímo ale využívá StorageService, které vždy posílá data ve formátu JSON. V případě chyby pak NULL.

public class ApiService : IApiService
{
	private readonly IStorageService storageService;

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

    public void GetArticles(Action<ListOfArticles, ResultState, Exception> callback)
    {
		var uri = new Uri("http://www.someapi.com/endpoint");
		var webClient = new WebClient();

        webClient.DownloadStringCompleted += (sender, e) =>
        {
            if (e.Error != null)
            {
                callback(storageService.Articles(null), ResultState.ConnectivityFailed, e.Error);
            }
            else
            {
                var jsonData = e.Result;

                try
                {
                    var articles = JsonConvert.DeserializeObject<ListOfArticles>(jsonData);
                    callback(storageService.Articles(articles), ResultState.Ok, null);
                }
                catch (Exception ex)
                {
                    callback(storageService.Articles(null), ResultState.JsonDeserialisationFailed, e.Error);
                }
            }
        };

		webClient.DownloadStringAsync(uri);
	}
}

StorageService.cs

Tato služba má za úkol buď uložit data, pokud nějaká dostane do interní IsolatedStorage, nebo se pokusit vrátit data z IsolatedStorage, pokud žádná data nedostane (a nějaká má). Data jsou vždy ukládána ve formátu JSON.

public class StorageService : IStorageService
{
	private readonly IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication();

    public ListOfArticles Articles(ListOfArticles articles)
    {
        if (articles != null)
            Save(articles, name);
        else
            articles = Load<ListOfArticles>(name);

        return articles;
    }

    private void Save<T>(T data, string name)
    {
        var serializer = new DataContractSerializer(typeof(T));

        string targetFileName = String.Format("{0}/{1}.dat", FolderName, name);
        if (!storage.DirectoryExists(FolderName))
            storage.CreateDirectory(FolderName);
        try
        {
            using (var targetFile = storage.CreateFile(targetFileName))
            {
                serializer.WriteObject(targetFile, data);
            }
        }
        catch (Exception ex)
        {
            storage.DeleteFile(targetFileName);
        }
    }

    private T Load<T>(string name)
    {
        T returnValue = default(T);

        var serializer = new DataContractSerializer(typeof(T));

        string targetFileName = String.Format("{0}/{1}.dat", FolderName, name);
        if (storage.FileExists(targetFileName))
            using (var sourceStream = storage.OpenFile(targetFileName, FileMode.Open))
            {
                returnValue = (T) serializer.ReadObject(sourceStream);
            }

        return returnValue; 
    }
}

Article.cs

public class Article : ObservableObject
{
    private string title;

    [JsonProperty("title")]
    public string Title 
    {
        get { return title; }
        set { Set(() => Title, ref title, value); }
    }
}

ListOfArticles.cs

public class ListOfArticles : ObservableObject
{
	private int count;
	private List<Article> items;

    public ListOfArticles()
    {
        Items = new List<Article>();
    }

    [JsonProperty("totalResults")]
    public int Count
    {
        get { return count; }
        set { Set(() => Count, ref count, value); }
    }

    [JsonProperty("articles")]
    public List<Article> Items
    {
        get { return items; }
        set { Set(() => Items, ref items, value); }
    }
}

Závěr a zhodnocení řešení

Výhodou popsaného řešení je meziukládání dat do interního úložiště. Celý tento pattern by se dal upravit tak, že by se data například cachovala po určitou dobu. Ukázka prezentovaná v tomto postu je schopná zajistit, že pokud se jednou uloží data do IsolatedStorage a uživatel je následně ztratí konektivitu, aplikace je i nadále schopná zobrazit alespoň neaktuální data.

Potřebujete pomoci?

Líbil se Vám článek? Máte dotaz nebo chcete v této oblasti s něčím pomoci? Neváhejte se na mě obrátit.

mirek@miroslavholec.cz

  • Řešení vývojářských problémů
  • Konzultace
  • Firemní školení a workshopy