OData en Strange Converter – deel 2

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…

  3 comments for “OData en Strange Converter – deel 2

  1. 28 april 2012 at 22:09

    Wederom erg nuttig, zit al te wachten op het vervolg ;)

    • Bob
      28 april 2012 at 22:48

      Die is ook net online gegaan :)

  2. 31 mei 2012 at 18:56

    Top! Ben eruit gekomen en heb het nu werkend :-)

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Verplichte velden zijn gemarkeerd met *

Zoals de meeste websites gebruiken we cookies om een meer persoonlijke en snelle service te bieden.

Wij gebruiken cookies zodat onze website meer efficiënt kan functioneren, om de prestaties te verbeteren en, eventueel, om op maat reclame van onze partners aan te bieden. Als u doorgaat gaan we ervan uit dat u akkoord gaat alle cookies te krijgen van onze website.

To accept cookies please Click To Continue