Dieser Artikel ist Tag #30 der Serie 31 Tage Mango von Jeff Blankenburg.
Der Originalartikel befindet sich hier: Day #30: Local Database.
Dieser Artikel wurde in der Originalserie von Gastautor Chris Woodruff geschrieben. Bei Twitter kann Chris unter @cwoodruff erreicht werden.
Was ist eine lokale Datenbank?
In der ersten Version von Windows Phone 7 konnten wir bereits eigene Daten speichern. Zur Ablage von relationalen Daten mussten wir aber entweder eigenen Code schreiben oder auf Lösungen wie die SterlingDB ausweichen. Das war eine Einschränkung für eine Reihe von Anwendungen.
In Windows Phone 7 Mango haben die Entwickler immer noch den Isolated Storage zur Speicherung von Daten und Informationen der Anwendung. Zusätzlich gibt es aber jetzt mit SQL CE die Möglichkeit, relationale Daten direkt über ein Feature des Betriebssystems abzuspeichern.
Genau wie die anderen Datenbanklösungen für das ursprüngliche Windows Phone 7 verwendet auch das in Mango eingebaute SQL CE den Isolated Storage zur Ablage der Daten im Gerät. Mehr über Isolated Storage können Sie hier erfahren. Der Umgang mit derartigen Daten ist eigentlich nichts neues: man verwendet einfach LINQ to SQL für alle Datenbankoperationen. LINQ to SQL wird für alle Operationen auf den Daten verwendet, sei es die Erstellung, Befüllung von Daten, Zugriff auf diese und natürlich Speichern und Löschen.
Eine gute Einführung in LINQ to SQL befindet sich hier in der MSDN.
Eine lokale Datenbank für eine Mango Anwendung einrichten
Wir beginnen diesmal mit einem Windows Phone Databound Application Projekt in Visual Studio 2010.
Wir hätten auch mit einem einfachen Windows Phone Application Projekt beginnen können. Ich wollte aber die weiteren Features der Databound Projektvorlage, wie zum Beispiel das Entwurfsmuster Model-View-ViewModel (MVVM).
ALs nächstes füllen wir unsere MainPage mit Leben, so dass wir Daten in die Datenbank eingeben können. Unsere Beispielanwendung wird dazu dienen, Ideen zu notieren und zu merken. Wir werden uns heute nicht mit den Einzelheiten der MainPage auseinandersetzen. Das folgende Codebeispiel enhält ohne weitere Erklärung den kompletten XAML Code der Main Page für unsere Ideensammlungs-App:
<phone:PhoneApplicationPage x:Class="_31DaysMangoStorage.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" mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768" d:DataContext="{d:DesignData SampleData/MainViewModelSampleData.xaml}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" shell:SystemTray.IsVisible="True"> <!--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 x:Name="ApplicationTitle" Text="31 Days of Mango" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock x:Name="PageTitle" Text="Idea Tracker" 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"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <!-- Bind the list box to the observable collection. --> <ListBox x:Name="toDoItemsListBox" ItemsSource="{Binding IdeaItems}" Grid.Row="0" Margin="12, 0, 12, 0" Width="440"> <ListBox.ItemTemplate> <DataTemplate> <Grid HorizontalAlignment="Stretch" Width="440"> <Grid.ColumnDefinitions> <ColumnDefinition Width="50" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="100" /> </Grid.ColumnDefinitions> <CheckBox IsChecked="{Binding IsComplete, Mode=TwoWay}" Grid.Column="0" VerticalAlignment="Center"/> <TextBlock Text="{Binding ItemName}" FontSize="{StaticResource PhoneFontSizeLarge}" Grid.Column="1" VerticalAlignment="Center"/> <Button Grid.Column="2" x:Name="deleteTaskButton" BorderThickness="0" Margin="0" Click="deleteTaskButton_Click"> <Image Source="appbar.delete.rest.png"/> </Button> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <TextBox x:Name="newIdeaTextBox" Grid.Column="0" Text="add new idea" FontFamily="{StaticResource PhoneFontFamilyLight}" GotFocus="newIdeaTextBox_GotFocus"/> <Button Content="add" Grid.Column="1" x:Name="newIdeaAddButton" Click="newIdeaAddButton_Click"/> </Grid> </Grid> </Grid> </phone:PhoneApplicationPage>
Damit unsere Anwendung kompiliert und ausführbar ist (ohne dass sie schon etwas machen würde), fügen wir im Code-Behind noch folgenden Code ein:
private void newIdeaTextBox_GotFocus(object sender, RoutedEventArgs e) { // Clear the text box when it gets focus. newIdeaTextBox.Text = String.Empty; } private void newIdeaAddButton_Click(object sender, RoutedEventArgs e) { } protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { // Call the base method. base.OnNavigatedFrom(e); }
Mit dem Data Context arbeiten
Der Data Context erlaubt uns die Arbeit mit der Datenbank und mit den Proxy Klassen, die unsere Datenbanktabellen repräsentieren. Der Data Context ist selbst eine Klasse und verwendet eine Reihe von Klassen, die wir für dieses Projekt erstellen werden. Die Tabellen-Objekte, welche unsere Datenbanktabellen repräsentieren werden, beinhalten eine Collection mit einem Element für jeden Eintrag in der Datenbanktabelle. Der Data Context liefert uns auch noch weitere Details über die Datenbank, wie zum Beispiel die Schlüsselspalten der Tabellen und die Relationen zwischen Tabellen.
Die lokale Datenbank im Handy dient nur zur Speicherung von Daten im Gerät. Sie hat keine Verbindung zum SQL Server 2008 R2, den Sie möglicherweise daheim oder in der Firma oder bei einem Dienstleister laufen haben.
Am Data Context ist eigentlich nicht viel mehr dran als der Connection String und Eigenschaften für jede der Tabellen unserer Datenbank. Der Code für den DataContext unserer Beispielanwendung sieht so aus:
public class IdeaDataContext : DataContext { // Specify the connection string as a static, used in main page and app.xaml. public static string DBConnectionString = "Data Source=isostore:/Ideas.sdf"; // Pass the connection string to the base class. public IdeaDataContext(string connectionString) : base(connectionString) { } // Specify a single table for the to-do items. public Table<IdeaItem> IdeaItems; }
Die Klasse IdeaItem werden wir im folgenden Abschnitt behandeln, wenn es um die Erstellung der Datenbank geht.
Die Datenbank erstellen
Anders als bei Anwendungen, die auf Ihrem PC oder im IIS 7 laufen, müssen Datenbanken in Windows Phone Mango erstellt und initalisiert werden, wenn die Anwendung zum ersten Mal auf dem Gerät gestartet wird. Wir schauen uns zunächst die Klassen an, die unsere Datenbanktabellen repräsentieren werden und gehen dann über zur Initalisierung der Datenbank.
Für jede Datenbanktabelle, die in der Datenbank auf dem Handy existieren soll, müssen wir eine neue Klasse anlegen. Da diese Klassen die in der Datenbank abgelegten Entities repräsentieren werden, nennen wir sie Entity Klassen („Entity“ lässt sich übersetzen mit „Dateneinheit“ oder „Datensatz“).
Um eine Entity Klasse zu bauen, müssen wir die folgenden beiden Interfaces implementieren:
- INotifyPropertyChanged — Das Interface INotifyPropertyChanged dient zur Unterrichtung von interessierten Objekten über die Änderung eines Eigenschaftswerts. Interessierte Objekte sind z.B. an eine Eigenschaft gebundene Elemente der Benutzerschnittstelle.
- INotifyPropertyChanging — Das Interface INotifyPropertyChanging unterrichtet interessierte Objekte darüber, dass eine Eigenschaft dabei ist, sich zu ändern.
Diese beiden Interfaces erlauben jeder Entity, den DataContext über eine anstehende bzw. abgeschlossene Änderung der Werte zu informieren. Wenn wir unsere Entities per XAML gebunden haben, werden die Änderungen durch Implementierung der beiden Interfaces gleichzeitig an der Oberfläche sichtbar.
Eine Entity Klasse muss mit dem Table Attribut annotiert werden. Zudem muss die Entity Klasse Eigenschaften für jede Spalte in der Datenbanktabelle haben. Die Eigenschaften müssen ebenfalls mit entsprechenden Metadaten annotiert werden. Die Metadaten beschreiben die Art der Datenbankspalte. Weiterhin muss jede Entity Klasse eine als Primärschlüssel gekennzeichnete Eigenschaft haben.
Im folgenden Codebeispiel findet sich die IdeaItem Entity Klasse, welche einen Datensatz der Tabelle IdeaItems repräsentieren wird. Die Tabelle selbst haben wir bereits im DataContext oben deklariert.
[Table] public class IdeaItem : INotifyPropertyChanged, INotifyPropertyChanging { private int _ideaItemId; [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "INT NOT NULL Identity", CanBeNull = false, AutoSync = AutoSync.OnInsert)] public int IdeaItemId { get { return _ideaItemId; } set { if (_ideaItemId != value) { NotifyPropertyChanging("IdeaItemId"); _ideaItemId = value; NotifyPropertyChanged("IdeaItemId"); } } } private string _itemName; [Column] public string ItemName { get { return _itemName; } set { if (_itemName != value) { NotifyPropertyChanging("ItemName"); _itemName = value; NotifyPropertyChanged("ItemName"); } } } private bool _isComplete; [Column] public bool IsComplete { get { return _isComplete; } set { if (_isComplete != value) { NotifyPropertyChanging("IsComplete"); _isComplete = value; NotifyPropertyChanged("IsComplete"); } } } [Column(IsVersion = true)] private Binary _version; public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangingEventHandler PropertyChanging; private void NotifyPropertyChanging(string propertyName) { if (PropertyChanging != null) { PropertyChanging(this, new PropertyChangingEventArgs(propertyName)); } } }
Zuletzt müssen wir die Datenbank neu anlegen, falls sie noch nicht existiert. In unserer Beispielanwendung machen wir das einfach im Konstruktor der App. Öffnen Sie die Datei App.xaml.cs und fügen Sie am Ende des Konstruktors folgenden Code ein:
using (IdeaDataContext db = new IdeaDataContext(IdeaDataContext.DBConnectionString)) { if (db.DatabaseExists() == false) { db.CreateDatabase(); } }
Wir haben jetzt im Isolated Storage eine Datenbank erstellt und initialisiert. Wichtig ist hierbei noch, dass aufgrund der geschützten Ausführung von Apps in Windows Phone die Datenbanken nicht zwischen Anwendungen geteilt werden können. Jede Anwendung hat nur ihre private, geschützte Sicht auf den Isolated Storage.
LINQ to SQL in Windows Phone Mango
Das Windows Phone 7.1 SDK implementiert viele, aber nicht alle Features von LINQ to SQL. Einige Punkte, die man im Hinterkopf behalten sollte, sind:
- ExecuteCommand ist nicht unterstützt.
- ADO.NET Objekte (wie z.B. der DataReader) sind nicht implementiert.
- Es werden nur die Datentypen des Microsoft SQL Server Compact Edition (SQL CE) unterstützt.
Um mehr Informationen über die Einschränkungen und Features von LINQ to SQL in Mango zu bekommen, lesen Sie diese MSDN Seite.
Auf die lokale Datenbank zugreifen
Um auf die Daten zuzugreifen oder diese zu manipulieren, müssen wir zunächst eine Instanz unserer Data Context Klasse erstellen und diese mit der Datenbank verbinden. In der Beispielanwendung machen wir das in der MainPage.xaml.cs mit Hilfe einer privaten Variable für den DataContext, einer Eigenschaft vom Typ ObservableCollection für die Datenbanktabelle der Ideen und etwas Code im Konstruktur wie folgt:
private IdeaDataContext ideaDB; private ObservableCollection<IdeaItem> _ideaItems; public ObservableCollection<IdeaItem> IdeaItems { get { return _ideaItems; } set { if (_ideaItems != value) { _ideaItems = value; NotifyPropertyChanged("IdeaItems"); } } } public MainPage() { InitializeComponent(); ideaDB = new IdeaDataContext(IdeaDataContext.DBConnectionString); this.DataContext = this; }
Um die Ideen in unserer lokalen Datenbank zu bekommen, verwenden wir eine LINQ to SQL Abfrage auf dem DataContext.
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { var ideaItemsInDB = from IdeaItem todo in ideaDB.IdeaItems select todo; IdeaItems = new ObservableCollection<IdeaItem>(ideaItemsInDB); base.OnNavigatedTo(e); }
Mit dem oben im XAML Code der MainPage.xaml bereits definierten Binding für die ListBox tauchen die Ideen jetzt in der Benutzeroberfläche auf.
Daten in der lokalen Datenbank speichern
Jetzt fehlt nur noch das Abspeichern unserer Ideen in der lokalen Datenbank. Da wir uns in der Beispielanwendung keine Gedanken über die Performance machen, werden wir die Daten erst beim Verlassen der Seite in der Datenbank speichern (Anm. leitning: wie in 31 Tage Mango | Tag #23: Das Ausführungsmodell erklärt, kann es unter Umständen erforderlich und sinnvoll sein, die Daten früher zu speichern. Beim Verlassen der Anwendung bekommt diese nur noch begrenzt Zeit vom Betriebssystem um hinter sich aufzuräumen. Wenn alle Daten erst dann gespeichert werden, kann das möglicherweise nicht mehr vollständig ausgeführt werden). Wir werden die Ideen in der ObservableCollection der MainPage (also in der Eigenschaft IdeaItems) halten. Das Hinzufügen einer neuen Idee passiert, wenn der Anwender auf den entsprechenden Button klickt. Wir fügen der Collection IdeaItems dann einen neuen Eintrag hinzu.
private void newIdeaAddButton_Click(object sender, RoutedEventArgs e) { IdeaItem newIdea = new IdeaItem { ItemName = newIdeaTextBox.Text }; IdeaItems.Add(newIdea); ideaDB.IdeaItems.InsertOnSubmit(newIdea); }
Wie gerade erwähnt, werden die Ideen des Anwenders nicht in der Datenbank gespeichert, bis die Main Page verlassen wird, indem entweder die Anwendung beendet wird oder der Anwender zu einer anderen Seite wechselt. Das eigentliche Speichern der Ideen in der Datenbank erledigen wir in der Beispielanwendung im OnNavigatedFrom Handler:
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { base.OnNavigatedFrom(e); ideaDB.SubmitChanges(); }
Zusammenfassung
So, das wars! Wir haben jetzt die ersten Schritte zur Persistierung relationaler Daten in Windows Phone gemacht. Wie werden Sie dieses Feature verwenden?
Um ein komplettes Windows Phone Projekt mit dem ganzen Code dieses Artikels herunterzuladen, klicken Sie auf den Download Code Button:
Morgen — im letzten Artikel der Serie — werden wir uns erfolgreiche Strategien zur Bekanntmachung und Vermarktung von Windows Phone Anwendungen ansehen.
Bis dahin!