Onlangs heb ik geblogd over OData en hoe dit op te zetten is als webservice. In deze blog wil ik dieper ingaan op het binnen halen van data van een OData service binnen een Windows Phone 7 app.
De OData geeft de inhoud en relaties terug van een database die ergens in de cloud staat. Dit gebeurd in een bepaald formaat, namelijk ATOM (XML) of JSON. Vandaag concentreer ik me alleen op het ATOM formaat, omdat dit het makkelijkst te verwerken is binnen de Windows Phone 7 app.
In mijn vorige blog had ik het over de Strange Converter, een app die het mogelijk maakt rare conversies te berekenen op de smartphone. Hiervoor heb ik een database en webservice opgezet beschreven in mijn vorige blog. Nu nog de data binnen halen in een Windows Phone 7 app.
OData op de WP7
Om data binnen te halen in een Windows Phone 7 app, hebben we een referentie nodig naar de webservice. Na het starten van een nieuw Windows Phone 7 Silverlight project kan je bij de opties van het project Add Service Reference kiezen.
In de wizard die volgt voer je de URL in van de webservice, in dit geval die van de Strange Converter: http://strangeconverterwebservice.apphb.com/StrangeConverterDataService.svc/
Let op dat je ook de naam van de Namespace zet naar iets dat betekenis heeft. Ik gebruik hier DataService als voorbeeld. Hier kan ik de service dan ook later mee aanroepen.
MVVM Binding
Om mijn data in de app weer te geven ga ik MVVM Binding gebruiken. MVVM staat voor Model View ViewModel en is een pattern wat gebruikt wordt onder andere in de Windows Phone 7 apps. Het voordeel hiervan is dat het zorgt voor lagen waardoor de designer en de developer zoveel mogelijk onafhankelijk van elkaar aan dezelfde app kunnen werken en daardoor sneller resultaat kunnen boeken. Meer informatie is te vinden op de MSDN website met video’s als tutorials hoe dit te gebruiken is. In het kort heeft de MVVM een View, een Model en een ViewModel.
- De View is je pagina in de app, de UserControl of de DataTemplate. Dit is wat de gebruiker ziet en waarmee de interactie plaats vindt tussen gebruiker en app.
- De Model is een class object dat een representatie is van je data. Deze classe heeft voornamelijk alleen properties (getters en setters) en wordt alleen gebruikt om de data op te slaan in het geheugen.
- De ViewModel wordt gebruikt als controle laag tussen de Model en de View. Vanuit de View gaan de commando’s naar de ViewModel. Deze haalt eventueel uit de Model de data en geeft deze terug aan de View.
In het onderstaande Cross-Functional Flowchart is te zien hoe de lagen zich verhouden tot elkaar met een implementatie voorbeeld eronder gebasseerd op de Windows Phone 7 app.
Zoals je kan zien kan de View ook direct de Model gebruiken. Dit kan doordat de ViewModel deze in de property Items bijvoorbeeld doorgeeft, maar ook door direct de Model aan te roepen. Wil je echter de data aanpassen voordat het aan de gebruiker wordt getoond, dan kan je het het beste via de ViewModel laten lopen, die bedoeld is voor juist dat soort aanpassingen. In je Model wil je eigenlijk geen rekening hoeven te houden met wat je laat zien, alleen de data zelf.
Goed, op naar de code. Ik ga in mijn app een ListBox implementeren die de items vanuit de webservice kan laten zien. Deze items zijn de conversies. Ik wil de ListBoxItems wel vorm geven en niet gewoon een TextBlock laten zijn. Mooi bordertje en wat kleurtjes moeten er natuurlijk wel in. Door die DateTemplate te gebruiken kan ik mijn data stijlen hoe ik dat wil.
Eerst maak ik mijn Model en mijn ViewModel.
Deze heb ik gemaakt aan de hand van mijn data model.
Mijn ConversionModel.cs ziet er als volgt uit:
public class ConversionModel: INotifyPropertyChanged { private UnitModel _TargetUnit; /// <summary> /// TargetUnit property; this property is used in the view to display its value using a Binding. /// </summary> /// <returns>UnitModel</returns> public UnitModel TargetUnit { get { return _TargetUnit; } set { if (value != _TargetUnit) { _TargetUnit = value; NotifyPropertyChanged("TargetUnit"); } } } private UnitModel _BaseUnit; /// <summary> /// BaseUnit property; this property is used in the view to display its value using a Binding. /// </summary> /// <returns>UnitModel</returns> public UnitModel BaseUnit { get { return _BaseUnit; } set { if (value != _BaseUnit) { _BaseUnit = value; NotifyPropertyChanged("BaseUnit"); } } } private decimal _TargetValue; /// <summary> /// TargetValue property; this property is used in the view to display its value using a Binding. /// </summary> /// <returns>double</returns> public decimal TargetValue { get { return _TargetValue; } set { if (value != _TargetValue) { _TargetValue = value; NotifyPropertyChanged("TargetValue"); } } } private decimal _BaseValue; /// <summary> /// BaseValue property; this property is used in the view to display its value using a Binding. /// </summary> /// <returns>double</returns> public decimal BaseValue { get { return _BaseValue; } set { if (value != _BaseValue) { _BaseValue = value; NotifyPropertyChanged("BaseValue"); } } } private int _Id; /// <summary> /// Id property; this property is used in the view to display its value using a Binding. /// </summary> /// <returns>int</returns> public int Id { get { return _Id; } set { if (value != _Id) { _Id = value; NotifyPropertyChanged("Id"); } } } public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(String propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (null != handler) { handler(this, new PropertyChangedEventArgs(propertyName)); } } }
Dit zijn alleen properties die gebruikt worden en een event. Het event is een implementatie van de INotifyPropertyChanged interface. Deze wordt bij elke wijziging van de data in een property aangeroepen, zodat de View weet dat het veranderd is en kan updaten.
De ViewModel
De code voor de ViewModel is gebasseerd op de Model.
public class ConversionsViewModel : INotifyPropertyChanged { // filename for saving the data in SaveData method private const string CACHE_FILE_NAME = "cacheddata.xml"; // Constructor public ConversionsViewModel() { this.Items = new ObservableCollection<ConversionModel>(); } // Items property of type ObservableCollection<> with ConversionModel items private ObservableCollection<ConversionModel> _Items = null; public ObservableCollection<ConversionModel> Items { get { return _Items; } private set { _Items = value; NotifyPropertyChanged("Items"); } } // used to provide information about the data loading process private bool _IsDataLoaded = false; public bool IsDataLoaded { get { return _IsDataLoaded; } private set { _IsDataLoaded = value; NotifyPropertyChanged("IsDataLoaded"); NotifyPropertyChanged("IsDataNotLoaded"); NotifyPropertyChanged("LoadingVisibility"); } } // negative value for the IsDataLoaded property, used in the ProgressBar public bool IsDataNotLoaded { get { return !this.IsDataLoaded; } } // used fore showing a loading element public Visibility LoadingVisibility { get { if (this.IsDataLoaded) return Visibility.Collapsed; else return Visibility.Visible; } } // loading the data public void LoadData() { // preload cached data this.Items = GetCachedData(); // create the webservice context StrangeConverterEntities context = new StrangeConverterEntities(new Uri("http://strangeconverterwebservice.apphb.com/StrangeConverterDataService.svc/", UriKind.Absolute)); // linq query to get the items nescesarry DataServiceQuery<Conversions> query = (DataServiceQuery<Conversions>)(from c in context.Conversions.Expand("Units").Expand("Units1") orderby c.Units.Name ascending select c); // collection to contain the items from the webservice DataServiceCollection<Conversions> conversionsCollection = new DataServiceCollection<Conversions>(context); conversionsCollection.LoadCompleted += new EventHandler<LoadCompletedEventArgs>((sender, e) => { // if there are more items, get the rest if (conversionsCollection.Continuation != null) { conversionsCollection.LoadNextPartialSetAsync(); } else { // create collection locally ObservableCollection<ConversionModel> conversions = new ObservableCollection<ConversionModel>(); // itterate throught the collection retreived from the webservice foreach (Conversions c in conversionsCollection) { // add item to the local collection as a new model conversions.Add(new ConversionModel() { Id = c.id, BaseUnit = new UnitModel() { Id = c.Units.id, Name = c.Units.Name, Description = c.Units.Description }, BaseValue = c.basevalue, TargetUnit = new UnitModel() { Id = c.Units1.id, Name = c.Units1.Name, Description = c.Units1.Description }, TargetValue = c.targetvalue }); } // set the property Items to the new collection, this firest the INotification event this.Items = conversions; // set the loading status this.IsDataLoaded = true; } }); // download the data asynchronously conversionsCollection.LoadAsync(query); } // method of getting the saved data from Isolated Storage private ObservableCollection<ConversionModel> GetCachedData() { ObservableCollection<ConversionModel> data = new ObservableCollection<ConversionModel>(); using (IsolatedStorageFile myIsolatedStorage = IsolatedStorageFile.GetUserStoreForApplication()) { if (myIsolatedStorage.FileExists(CACHE_FILE_NAME)) { using (IsolatedStorageFileStream stream = myIsolatedStorage.OpenFile(CACHE_FILE_NAME, FileMode.Open)) { try { System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(ObservableCollection<ConversionModel>)); data = (ObservableCollection<ConversionModel>)serializer.Deserialize(stream); } catch (Exception e) { throw e; } } } else { data = new ObservableCollection<ConversionModel>(); } } return data; } // save the data to Isolated Storage using serialization public void SaveData() { XmlWriterSettings xmlWriterSettings = new XmlWriterSettings(); xmlWriterSettings.Indent = true; using (IsolatedStorageFile myIsolatedStorage = IsolatedStorageFile.GetUserStoreForApplication()) { using (IsolatedStorageFileStream stream = myIsolatedStorage.OpenFile(CACHE_FILE_NAME, FileMode.Create)) { System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(ObservableCollection<ConversionModel>)); using (XmlWriter xmlWriter = XmlWriter.Create(stream, xmlWriterSettings)) { serializer.Serialize(xmlWriter, this.Items); } } } } // the INotifyPropertyChanged PropertyChanged event public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(String propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (null != handler) { handler(this, new PropertyChangedEventArgs(propertyName)); } } }
Hier is vooral de Items property belangrijk, omdat deze een ObservableCollection bevat van de Model. De ObservableCollection is een collection met een extra optie om te ‘vertellen’ dat een property veranderd is door middel van ook het event. Ook dit event PropertyChanged wordt dan weer gebruikt door de View om zichzelf te updaten met de nieuwe data.
De LoadData() methode hier roept de webservice aan en download dan asynchroon de data. Deze wordt dan omgezet in ConversionModel objecten en toegevoed aan de collectie. Ik gebruik hier een anonieme method call om de code uit te voeren als de data is gedownload. Bij het aanroepen van de conversionsCollection.LoadCompleted += new EventHandler<LoadCompletedEventArgs>((sender, e) => had ik ook kunnen verwijzen naar een ander methode in de class.
De View
De view is onze ListBox en daarin de DataTemplate. Deze wordt in de XAML toegevoegd.
<StackPanel Orientation="Vertical" Name="PanelConversions" DataContext="{Binding Source={StaticResource ConversionsViewModel}}" d:DataContext="{d:DesignData SampleData/ConversionsViewModelSampleData.xaml}"> <ListBox Margin="0,0,-12,0" ItemsSource="{Binding Items}" Height="530" local:TiltEffect.IsTiltEnabled="True" Tap="ListBox_Tap"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal" Margin="0,0,0,12"> <Rectangle Opacity="0.7" Fill="#0078ff" Width="10" /> <Border Height="60" BorderBrush="Black" BorderThickness="1" Background="White" Opacity="0.7"> <StackPanel Margin="0,0,0,7" Width="432" Orientation="Horizontal" VerticalAlignment="Center"> <TextBlock Text="How many " Style="{StaticResource PhoneTextTitle2Style}" FontSize="28" Foreground="Black" Margin="12,0,0,0" TextWrapping="Wrap" /> <TextBlock Text="{Binding BaseUnit.Description}" TextWrapping="Wrap" FontSize="28" Style="{StaticResource PhoneTextTitle2Style}" Foreground="Black" Margin="0,0,0,0"/> <TextBlock Text="s in a " Style="{StaticResource PhoneTextTitle2Style}" FontSize="28" Foreground="Black" Margin="0,0,0,0" TextWrapping="Wrap"/> <TextBlock Text="{Binding TargetUnit.Description}" TextWrapping="Wrap" FontSize="28" Style="{StaticResource PhoneTextTitle2Style}" Foreground="Black" Margin="0,0,0,0"/> </StackPanel> </Border> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </StackPanel>
De XAML bevat eerst een StackPanel. Dit is onze container voor de DataContext. De DataContext is de property om te verwijzen naar de data. Deze staat in dit geval in de StackPanel zodat ik eventueel later nog een ProgressBar kan toevoegen boven de list. Zou ik het in de list stoppen, dan kan die ProgressBar niet meer ge-bind worden aan de IsDataNotLoaded property.
In de ListBox zien we dat de ItemsSource is gekoppeld aan de Items property van de ViewModel. Dit is dus de ObservableCollection van het type ConversionModel. Hij zal dus alle items in deze collection laden in de List. Nu wil ik voor elke Item in die collection ook deze stijlen. Dat doe ik in de DataTemplate. Deze bevat de elementen die ik wel of niet kan Binden aan de data. Zo zijn er meerdere TextBoxes toegevoegd waarvan er enkele zijn gekoppeld aan de data. De andere vullen het totaal plaatje aan.
De DataContext is hier aan een StaticResource gekoppeld die ik in de XAML hoger in de pagina heb aangemaakt.
<phone:PhoneApplicationPage.Resources> <viewmodel:ConversionsViewModel x:Key="ConversionsViewModel" /> </phone:PhoneApplicationPage.Resources>
Voor de duidelijkheid, de DataContext moet ik ook zetten in de CodeBeside van deze pagina. In dit geval de MainPage.cs.
// Constructor public MainPage() { InitializeComponent(); // Set the data context of the listbox control to the sample data this.Loaded += new RoutedEventHandler(MainPage_Loaded); } // Load data for the ViewModel Items private void MainPage_Loaded(object sender, RoutedEventArgs e) { this.PanelConversions.DataContext = App.ConversionsViewModel; if (!App.ConversionsViewModel.IsDataLoaded) { App.ConversionsViewModel.LoadData(); } }
Hier zet ik de this.PanelConversions.DataContext = App.ConversionsViewModel waarin de App.ConversionsViewModel gedefinieerd staat in de App.cs.
private static ConversionsViewModel _ConversionsViewModel = null; /// <summary> /// A static ViewModel used by the views to bind against. /// </summary> /// <returns>The MainViewModel object.</returns> public static ConversionsViewModel ConversionsViewModel { get { // Delay creation of the view model until necessary if (_ConversionsViewModel == null) _ConversionsViewModel = new ConversionsViewModel(); return _ConversionsViewModel; } }
Dit is om te zorgen dat het overal in de app beschikbaar is.
Bij het aanroepen van de LoadData van de ViewModel wordt de data geladen. Wanneer de data is geladen zal doordat het PropertyChanged event wordt afgevuurd de View geüpdatet, de ListBox dus. De LoadData staat in de Loaded event van de pagina, dus als de pagina wordt geladen, wordt automatisch de data van het internet gehaald.
Ik kan zelf mijn database aanpassen, dus ik kan op afstand de conversies toevoegen, wijzigen of verwijderen. Dit zorgt ervoor dat ik zonder de app te moeten updaten de data kan aanpassen. Ik had het ook hard-coded kunnen doen, maar dit geeft mij meer mogelijkheden en is ook een goede oefening
Tot zover het laden van de data. Volgende keer ga ik dieper in op het opslaan van data vanuit de app naar de OData service toe.
Wordt vervolgd…
Wederom erg nuttig, zit al te wachten op het vervolg
Die is ook net online gegaan
Top! Ben eruit gekomen en heb het nu werkend