Windows Phone + Isolated Storage + Api service
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.