Georg fährt extra nach Berlin um Steve Ballmer zu treffen

31 Tage Mango | Tag #30: Lokale Datenbank

Die­ser Arti­kel ist Tag #30 der Serie 31 Tage Man­go von Jeff Blan­ken­burg.

Der Ori­gi­nal­ar­ti­kel befin­det sich hier: Day #30: Local Data­ba­se.

Die­ser Arti­kel wur­de in der Ori­gi­nal­se­rie von Gast­au­tor Chris Woo­d­ruff geschrie­ben. Bei Twit­ter kann Chris unter @cwoodruff erreicht wer­den.

Was ist eine lokale Datenbank?

In der ers­ten Ver­si­on von Win­dows Pho­ne 7 konn­ten wir bereits eige­ne Daten spei­chern. Zur Abla­ge von rela­tio­na­len Daten muss­ten wir aber ent­we­der eige­nen Code schrei­ben oder auf Lösun­gen wie die Ster­lingDB aus­wei­chen. Das war eine Ein­schrän­kung für eine Rei­he von Anwen­dun­gen.

In Win­dows Pho­ne 7 Man­go haben die Ent­wick­ler immer noch den Iso­la­ted Sto­rage zur Spei­che­rung von Daten und Infor­ma­tio­nen der Anwen­dung. Zusätz­lich gibt es aber jetzt mit SQL CE die Mög­lich­keit, rela­tio­na­le Daten direkt über ein Fea­ture des Betriebs­sys­tems abzu­spei­chern.

Genau wie die ande­ren Daten­bank­lö­sun­gen für das ursprüng­li­che Win­dows Pho­ne 7 ver­wen­det auch das in Man­go ein­ge­bau­te SQL CE den Iso­la­ted Sto­rage zur Abla­ge der Daten im Gerät. Mehr über Iso­la­ted Sto­rage kön­nen Sie hier erfah­ren. Der Umgang mit der­ar­ti­gen Daten ist eigent­lich nichts neu­es: man ver­wen­det ein­fach LINQ to SQL für alle Daten­bank­ope­ra­tio­nen. LINQ to SQL wird für alle Ope­ra­tio­nen auf den Daten ver­wen­det, sei es die Erstel­lung, Befül­lung von Daten, Zugriff auf die­se und natür­lich Spei­chern und Löschen.

Eine gute Ein­füh­rung in LINQ to SQL befin­det sich hier in der MSDN.

Eine lokale Datenbank für eine Mango Anwendung einrichten

Wir begin­nen dies­mal mit einem Win­dows Pho­ne Data­bound App­li­ca­ti­on Pro­jekt in Visu­al Stu­dio 2010.

Wir hät­ten auch mit einem ein­fa­chen Win­dows Pho­ne App­li­ca­ti­on Pro­jekt begin­nen kön­nen. Ich woll­te aber die wei­te­ren Fea­tures der Data­bound Pro­jekt­vor­la­ge, wie zum Bei­spiel das Ent­wurfs­mus­ter Model-View-View­Mo­del (MVVM).

ALs nächs­tes fül­len wir unse­re Main­Page mit Leben, so dass wir Daten in die Daten­bank ein­ge­ben kön­nen. Unse­re Bei­spiel­an­wen­dung wird dazu die­nen, Ide­en zu notie­ren und zu mer­ken. Wir wer­den uns heu­te nicht mit den Ein­zel­hei­ten der Main­Page aus­ein­an­der­set­zen. Das fol­gen­de Code­bei­spiel enhält ohne wei­te­re Erklä­rung den kom­plet­ten XAML Code der Main Page für unse­re Ide­en­samm­lungs-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 unse­re Anwen­dung kom­pi­liert und aus­führ­bar ist (ohne dass sie schon etwas machen wür­de), fügen wir im Code-Behind noch fol­gen­den 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 Con­text erlaubt uns die Arbeit mit der Daten­bank und mit den Pro­xy Klas­sen, die unse­re Daten­bank­ta­bel­len reprä­sen­tie­ren. Der Data Con­text ist selbst eine Klas­se und ver­wen­det eine Rei­he von Klas­sen, die wir für die­ses Pro­jekt erstel­len wer­den. Die Tabel­len-Objek­te, wel­che unse­re Daten­bank­ta­bel­len reprä­sen­tie­ren wer­den, beinhal­ten eine Collec­tion mit einem Ele­ment für jeden Ein­trag in der Daten­bank­ta­bel­le. Der Data Con­text lie­fert uns auch noch wei­te­re Details über die Daten­bank, wie zum Bei­spiel die Schlüs­sel­spal­ten der Tabel­len und die Rela­tio­nen zwi­schen Tabel­len.

Die loka­le Daten­bank im Han­dy dient nur zur Spei­che­rung von Daten im Gerät. Sie hat kei­ne Ver­bin­dung zum SQL Ser­ver 2008 R2, den Sie mög­li­cher­wei­se daheim oder in der Fir­ma oder bei einem Dienst­leis­ter lau­fen haben.

Am Data Con­text ist eigent­lich nicht viel mehr dran als der Con­nec­tion String und Eigen­schaf­ten für jede der Tabel­len unse­rer Daten­bank. Der Code für den Dat­a­Con­text unse­rer Bei­spiel­an­wen­dung 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 Klas­se Ide­aI­tem wer­den wir im fol­gen­den Abschnitt behan­deln, wenn es um die Erstel­lung der Daten­bank geht.

Die Datenbank erstellen

Anders als bei Anwen­dun­gen, die auf Ihrem PC oder im IIS 7 lau­fen, müs­sen Daten­ban­ken in Win­dows Pho­ne Man­go erstellt und ini­ta­li­siert wer­den, wenn die Anwen­dung zum ers­ten Mal auf dem Gerät gestar­tet wird. Wir schau­en uns zunächst die Klas­sen an, die unse­re Daten­bank­ta­bel­len reprä­sen­tie­ren wer­den und gehen dann über zur Ini­ta­li­sie­rung der Daten­bank.

Für jede Daten­bank­ta­bel­le, die in der Daten­bank auf dem Han­dy exis­tie­ren soll, müs­sen wir eine neue Klas­se anle­gen. Da die­se Klas­sen die in der Daten­bank abge­leg­ten Ent­i­ties reprä­sen­tie­ren wer­den, nen­nen wir sie Enti­ty Klas­sen („Enti­ty“ lässt sich über­set­zen mit „Daten­ein­heit“ oder „Daten­satz“).

Um eine Enti­ty Klas­se zu bau­en, müs­sen wir die fol­gen­den bei­den Inter­faces imple­men­tie­ren:

  • INo­ti­fy­Proper­ty­Ch­an­ged — Das Inter­face INo­ti­fy­Proper­ty­Ch­an­ged dient zur Unter­rich­tung von inter­es­sier­ten Objek­ten über die Ände­rung eines Eigen­schafts­werts. Inter­es­sier­te Objek­te sind z.B. an eine Eigen­schaft gebun­de­ne Ele­men­te der Benut­zer­schnitt­stel­le.
  • INo­ti­fy­Proper­ty­Ch­an­ging — Das Inter­face INo­ti­fy­Proper­ty­Ch­an­ging unter­rich­tet inter­es­sier­te Objek­te dar­über, dass eine Eigen­schaft dabei ist, sich zu ändern.

Die­se bei­den Inter­faces erlau­ben jeder Enti­ty, den Dat­a­Con­text über eine anste­hen­de bzw. abge­schlos­se­ne Ände­rung der Wer­te zu infor­mie­ren. Wenn wir unse­re Ent­i­ties per XAML gebun­den haben, wer­den die Ände­run­gen durch Imple­men­tie­rung der bei­den Inter­faces gleich­zei­tig an der Ober­flä­che sicht­bar.

Eine Enti­ty Klas­se muss mit dem Table Attri­but anno­tiert wer­den. Zudem muss die Enti­ty Klas­se Eigen­schaf­ten für jede Spal­te in der Daten­bank­ta­bel­le haben. Die Eigen­schaf­ten müs­sen eben­falls mit ent­spre­chen­den Meta­da­ten anno­tiert wer­den. Die Meta­da­ten beschrei­ben die Art der Daten­bank­spal­te. Wei­ter­hin muss jede Enti­ty Klas­se eine als Pri­mär­schlüs­sel gekenn­zeich­ne­te Eigen­schaft haben.

Im fol­gen­den Code­bei­spiel fin­det sich die Ide­aI­tem Enti­ty Klas­se, wel­che einen Daten­satz der Tabel­le Ide­aI­tems reprä­sen­tie­ren wird. Die Tabel­le selbst haben wir bereits im Dat­a­Con­text oben dekla­riert.

[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üs­sen wir die Daten­bank neu anle­gen, falls sie noch nicht exis­tiert. In unse­rer Bei­spiel­an­wen­dung machen wir das ein­fach im Kon­struk­tor der App. Öff­nen Sie die Datei App.xaml.cs und fügen Sie am Ende des Kon­struk­tors fol­gen­den Code ein:

using (IdeaDataContext db = new IdeaDataContext(IdeaDataContext.DBConnectionString))
{
   if (db.DatabaseExists() == false)
   {
      db.CreateDatabase();
   }
} 

Wir haben jetzt im Iso­la­ted Sto­rage eine Daten­bank erstellt und initia­li­siert. Wich­tig ist hier­bei noch, dass auf­grund der geschütz­ten Aus­füh­rung von Apps in Win­dows Pho­ne die Daten­ban­ken nicht zwi­schen Anwen­dun­gen geteilt wer­den kön­nen. Jede Anwen­dung hat nur ihre pri­va­te, geschütz­te Sicht auf den Iso­la­ted Sto­rage.

LINQ to SQL in Windows Phone Mango

Das Win­dows Pho­ne 7.1 SDK imple­men­tiert vie­le, aber nicht alle Fea­tures von LINQ to SQL. Eini­ge Punk­te, die man im Hin­ter­kopf behal­ten soll­te, sind:

  • Exe­cu­te­Com­mand ist nicht unter­stützt.
  • ADO.NET Objek­te (wie z.B. der DataRe­ader) sind nicht imple­men­tiert.
  • Es wer­den nur die Daten­ty­pen des Micro­soft SQL Ser­ver Com­pact Edi­ti­on (SQL CE) unter­stützt.

Um mehr Infor­ma­tio­nen über die Ein­schrän­kun­gen und Fea­tures von LINQ to SQL in Man­go zu bekom­men, lesen Sie die­se MSDN Sei­te.

Auf die lokale Datenbank zugreifen

Um auf die Daten zuzu­grei­fen oder die­se zu mani­pu­lie­ren, müs­sen wir zunächst eine Instanz unse­rer Data Con­text Klas­se erstel­len und die­se mit der Daten­bank ver­bin­den. In der Bei­spiel­an­wen­dung machen wir das in der MainPage.xaml.cs mit Hil­fe einer pri­va­ten Varia­ble für den Dat­a­Con­text, einer Eigen­schaft vom Typ Obser­v­a­ble­Collec­tion für die Daten­bank­ta­bel­le der Ide­en und etwas Code im Kon­struk­tur 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 Ide­en in unse­rer loka­len Daten­bank zu bekom­men, ver­wen­den wir eine LINQ to SQL Abfra­ge auf dem Dat­a­Con­text.

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 defi­nier­ten Bin­ding für die List­Box tau­chen die Ide­en jetzt in der Benut­zer­ober­flä­che auf.

Daten in der lokalen Datenbank speichern

Jetzt fehlt nur noch das Abspei­chern unse­rer Ide­en in der loka­len Daten­bank. Da wir uns in der Bei­spiel­an­wen­dung kei­ne Gedan­ken über die Per­for­mance machen, wer­den wir die Daten erst beim Ver­las­sen der Sei­te in der Daten­bank spei­chern (Anm. leit­ning: wie in 31 Tage Man­go | Tag #23: Das Aus­füh­rungs­mo­dell erklärt, kann es unter Umstän­den erfor­der­lich und sinn­voll sein, die Daten frü­her zu spei­chern. Beim Ver­las­sen der Anwen­dung bekommt die­se nur noch begrenzt Zeit vom Betriebs­sys­tem um hin­ter sich auf­zu­räu­men. Wenn alle Daten erst dann gespei­chert wer­den, kann das mög­li­cher­wei­se nicht mehr voll­stän­dig aus­ge­führt wer­den). Wir wer­den die Ide­en in der Obser­v­a­ble­Collec­tion der Main­Page (also in der Eigen­schaft Ide­aI­tems) hal­ten. Das Hin­zu­fü­gen einer neu­en Idee pas­siert, wenn der Anwen­der auf den ent­spre­chen­den But­ton klickt. Wir fügen der Collec­tion Ide­aI­tems dann einen neu­en Ein­trag hin­zu.

private void newIdeaAddButton_Click(object sender, RoutedEventArgs e)
{
   IdeaItem newIdea = new IdeaItem { ItemName = newIdeaTextBox.Text };

   IdeaItems.Add(newIdea);
   ideaDB.IdeaItems.InsertOnSubmit(newIdea);
} 

Wie gera­de erwähnt, wer­den die Ide­en des Anwen­ders nicht in der Daten­bank gespei­chert, bis die Main Page ver­las­sen wird, indem ent­we­der die Anwen­dung been­det wird oder der Anwen­der zu einer ande­ren Sei­te wech­selt. Das eigent­li­che Spei­chern der Ide­en in der Daten­bank erle­di­gen wir in der Bei­spiel­an­wen­dung im OnNavi­ga­ted­From Hand­ler:

protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
{
    base.OnNavigatedFrom(e);
    ideaDB.SubmitChanges();
}

Zusammenfassung

So, das wars! Wir haben jetzt die ers­ten Schrit­te zur Per­sis­tie­rung rela­tio­na­ler Daten in Win­dows Pho­ne gemacht. Wie wer­den Sie die­ses Fea­ture ver­wen­den?

Um ein kom­plet­tes Win­dows Pho­ne Pro­jekt mit dem gan­zen Code die­ses Arti­kels her­un­ter­zu­la­den, kli­cken Sie auf den Down­load Code But­ton:

Mor­gen — im letz­ten Arti­kel der Serie — wer­den wir uns erfolg­rei­che Stra­te­gi­en zur Bekannt­ma­chung und Ver­mark­tung von Win­dows Pho­ne Anwen­dun­gen anse­hen.

Bis dahin!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert