Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An Address Book Application Made in MVVM For Metro Style App in Windows 8 Part 2

0.00/5 (No votes)
29 May 2012 1  
This is the Second Part XML file as data source in Windows8

This is the second part of the article on using an XML file as a data source in Windows 8, check the previous article HERE

In the previous series we saw how to create a basic layout of our application using MVVM. We hooked up the buttons of our "View" with our "ViewModel". Now we need to connect our "ViewModel" with our "Model".

A "Model" is a representation of your data. Its a simple "class" which represent a single data entity. In our app, we will use our "Contact.cs" class that we built in earlier post.

using System;
  using System.Collections.Generic;
  using System.ComponentModel;
  using System.Linq;
  using System.Text;
  using System.Threading.Tasks;
  namespace Mvvm3_Basic.Model
  {
  public class Contact : INotifyPropertyChanged
  {
  private string _guid;
  public string CGuid { get { return _guid; } set { _guid = value; CP("CGuid"); } }
  private string _name;
  public string Name { get { return _name; } set { _name = value; CP("Name"); } }
  private string _email;
  public string Email { get { return _email; } set { _email = value; CP("Email"); } } private void CP(string s)
  {
  if (PropertyChanged != null)
  PropertyChanged(this, new PropertyChangedEventArgs(s));
  }
  public event PropertyChangedEventHandler PropertyChanged;
  }
  }

Before we begin updating our ViewModel, we need to make sure that we have a valid data source. Our data source will be a simple XML file.

Read and writing files in Metro can be tricky. You will have to read the documentation at MSDN.

We will first create our XML file. Right click on Project's name, Add -> New Item -> Data -> XML File. Name the XML file as "ContactXML.xml". Hit Ok.

The structure of file will be like:

<?xml version="1.0" encoding="utf-8">
  <Contacts>
  <Contact>
  <CGuid></CGuid>
  <Name></Name
  <Email></Email>
  </Contact>
  </Contacts>

Note: We will maintain this structure. But since we do not have any data and we will be adding data at run-time only, remove the <Contact> node along with its children. So you are left with only <Contacts></Contacts> (note the plural).

It is very important that we place this XML file exactly in the same folder where our EXE will be located. The current location of this XML file is not good. Once you save the XML file, move the XML file to your app's executable directory. The app we are building is pretty simple and do not use any other directories other than its own. If you do not copy the XML then the app won't run. Usually its {project}/bin/debug/appx/.

After our XML is ready and is in place, the first step is to write a class which will do all the reading and writing of contacts to and from our XML file. Right click on "ViewModel" folder, Add -> Class. Name the class as "DB.cs"

We will be adding a lot of code to this class, so pay close attention.

  1. Create a list of Contact class as ObservableCollection<Contact> as: (for observablecollection, you'll need System.Collections.ObjectModel and for "Contact" you'll need using Mvvm3_Basic.Model => it's in my case, yours might be different)
  2. You will also need to add a lot of other namespaces. Have a look at the screenshot.
    using System;
      using System.Collections.Generic;
      using System.Collections.ObjectModel;
      using System.Linq;
      using System.Text;
      using System.Threading.Tasks;using Mvvm3_Basic.Model;
        using System.Xml;
        using System.Xml.Linq;
        using Windows.ApplicationModel;
        using Windows.Data.Xml.Dom;
        using Windows.Storage;
        using System.IO;namespace Mvvm3_Basic.ViewModel
        {
        class DB
        {
        public ObservableCollection<Contact> contactlist { get; set; }}}
  3. For testing purpose, we will be only inserting records in our XML for this time. So insert one record in your XML file. Example:
    <Contact>
        <CGuid>c3d471aa-2f38-4861-ae1e-8614a21d0734</CGuid>
        <Name>Harry</Name>
        <Email>harry@hogwarts.com</Email>
    </Contact>
     inside <Contacts></Contacts>
  4. Next we add code to read from the XML. The method is called: GetContactList()
    namespace Mvvm3_Basic.ViewModel
      {
    class DB
    {
    public ObservableCollection<Contact> contactlist { get; set; } public async void GetContactList()
        {
        var sf = await Package.Current.InstalledLocation.GetFileAsync(@"ContactXML.xml");
        var file = await sf.OpenAsync(FileAccessMode.Read);
        Stream inStream = file.AsStreamForRead()XDocument xdoc = XDocument.Load(inStream);
        var contacts = (from contact in xdoc.Descendants("Contact")
        select new
        {
        CGuid = contact.Element("CGuid").Value,
        Name = contact.Element("Name").Value,
        Email = contact.Element("Email").Value
        });if (contactlist == null) contactlist = new ObservableCollection<Contact>();
        contactlist.Clear(); foreach (var c in contacts)
        {
        contactlist.Add(new Contact { CGuid = c.CGuid, Name = c.Name, Email = c.Email });
        }
        }
        }
        }
  5. The class ready records "asynchronously" from our XML file using XDocument. Then with a little-bit of LINQ and a for-loop, we create a list of "Contact" objects a put it in our ObservableCollection. MSDN contains a lot of documentation on reading "async".
  6. Next we add a method to insert a "Contact" in our XML file.
    public async void Save(Contact c)
    {
    var sf = await Package.Current.InstalledLocation.GetFileAsync(@"ContactXML.xml");
    XmlDocument xmlDoc;
    using (var stream = await sf.OpenAsync(FileAccessMode.ReadWrite))
    {
    xmlDoc = await XmlDocument.LoadFromFileAsync(sf);
    XmlElement root = xmlDoc.DocumentElement;
    XmlElement xe = xmlDoc.CreateElement("Contact");
    XmlElement cguid = xmlDoc.CreateElement("CGuid");
    cguid.InnerText = Guid.NewGuid().ToString();
    XmlElement name = xmlDoc.CreateElement("Name");
    name.InnerText = c.Name;
    XmlElement email = xmlDoc.CreateElement("Email");
    email.InnerText = c.Email;
    xe.AppendChild(cguid);
    xe.AppendChild(name);
    xe.AppendChild(email);
    root.AppendChild(xe);
    }
    if (xmlDoc != null)
    await xmlDoc.SaveToFileAsync(sf);
    }
    }
    }
  7. The insert, update and delete operations will not use XDocument. Instead we will use the XmlDocument and deal with various XmlElement.
  8. Next we add an Update method. Our contact will be updated with respect to "CGuid". It acts as our primary key here.
     public async void Update(Contact c)
    {
    var sf = await Package.Current.InstalledLocation.GetFileAsync(@"ContactXML.xml");
    XmlDocument xmlDoc;
    using (var stream = await sf.OpenAsync(FileAccessMode.ReadWrite))
    {
    xmlDoc = await XmlDocument.LoadFromFileAsync(sf);
    XmlElement root = xmlDoc.DocumentElement;
    IXmlNode xee = root.SelectSingleNode("//Contact/CGuid[.='" + c.CGuid + "']");
    xee.NextSibling.NextSibling.InnerText = c.Email;
    xee.NextSibling.InnerText = c.Name;
    }
    if (xmlDoc != null)
    await xmlDoc.SaveToFileAsync(sf);
    }
    }
    }
  9. We use XPath to find and update our contact. You will need to brush up your XPaths!
  10. Next we add a "Delete" method. As with update, delete will be done with respect to CGuid.
    public async void Delete(Contact c)
    {
    var sf = await Package.Current.InstalledLocation.GetFileAsync(@"ContactXML.xml");
    XmlDocument xmlDoc;
    using (var stream = await sf.OpenAsync(FileAccessMode.ReadWrite))
    {
    xmlDoc = await XmlDocument.LoadFromFileAsync(sf);
    XmlElement root = xmlDoc.DocumentElement;
    //IXmlNode xee = root.SelectSingleNode("//Contact/Name[.='" + c.Name + "'] | //Contact/Email[.='" + c.Email + "']");
    IXmlNode xee = root.SelectSingleNode("//Contact/CGuid[.='" + c.CGuid + "']");
    xee.ParentNode.RemoveChild(xee.NextSibling.NextSibling);
    xee.ParentNode.RemoveChild(xee.NextSibling);
    xee.ParentNode.RemoveChild(xee); IXmlNode clup = root.SelectSingleNode("//*[not(node())]");
      clup.ParentNode.RemoveChild(clup);
      }
      if (xmlDoc != null)
      await xmlDoc.SaveToFileAsync(sf);
      }
      }
      }
    
  11. Well that's pretty much to our DB.cs class. We will now setup our ViewModel class.

Lets work with the ViewModel now.

Add a property called SelectedContact to our ViewModel as shown. This property will maintain the state. In simple terms, which ever "Contact" you are working with in the UI, this property will maintain details about that in the ViewModel. Also, create an object of DB.cs. We need this object to work with our data source.

DB db = new DB();    //at class level private Contact _ctc = new Contact();
  public Contact SelectedContact
  {
  get { return _ctc; }
  set
  {
  _ctc = value;
  if (PropertyChanged != null)
  PropertyChanged(this, new PropertyChangedEventArgs("SelectedContact")); }
  }

Now let us modify our methods to incorporate the new data source.

  1. First we will modify OnAdd(). This method will return an empty contact. This is useful because in our "View" (UI), we have an "Add New" button. When that button is clicked, we need to clear our textboxes so that user can enter data. Since our textbox in our View (UI) will be bound to SelectedContact property, returning an empty string will automatically clear the textbox of any value. (neat eh!)
    void OnAdd(object obj)
    {
    Contact c = new Contact { Name = string.Empty, Email = string.Empty };
    SelectedContact = c;
    PropertyChanged(this, new PropertyChangedEventArgs("SelectedContact")); Debug.WriteLine("Add Called!");
      }
  2. Next we will modify the save method.
     void OnSave(object obj)
    {
    db.Save(SelectedContact);
    if (PropertyChanged != null)
    PropertyChanged(this, new PropertyChangedEventArgs("SelectedContact")); Debug.WriteLine("Save Called!");
        }
  3. Next is the update method.
    void OnUpdate(object obj)
    {
    db.Update(SelectedContact);
    if (PropertyChanged != null)
    PropertyChanged(this, new PropertyChangedEventArgs("Contacts"));Debug.WriteLine("Update Called!");
        }
  4. Next comes the delete method.
    void OnDelete(object obj)
    {
    db.Delete(SelectedContact);
    if (PropertyChanged != null)
    PropertyChanged(this, new PropertyChangedEventArgs("SelectedContact")); Debug.WriteLine("Delete Called!");
        }
  5. Finally the important GetContact() method.
    void GetContact(object obj)
    {
    if (Contacts == null) Contacts = new ObservableCollection<Contact>();
    Contacts.Clear();
    db.GetContactList();
    Contacts = db.contactlist;
    if (PropertyChanged != null)
    PropertyChanged(this, new PropertyChangedEventArgs("Contacts")); Debug.WriteLine("Refresh called!");
        }

The final step will be building the View i.e., our UI. So lets get started. You will need to modify the existing UI.

In our existing UI, we only have a stackpanel containing our Buttons. Now we are going to add one ListBox (to hold the list of our contacts) and one Grid (which is our form to work with the contacts). When we select any contact from the ListBox, the details are loaded in the Form. At the same time, the "SelectedContact" property of our ViewModel is also set to the contact selected from the ListBox. This is achieved using TwoWay Binding with little help from our INotifyPropertyChanged interface.

  1. We divide the existing Grid in to a proper layout
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"> <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
      <Grid.RowDefinitions>
      <RowDefinition Height="25*"/>
      <RowDefinition Height="231*"/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
      <ColumnDefinition Width="205*"/>
      <ColumnDefinition Width="478*"/>
      </Grid.ColumnDefinitions>
  2. Next we add our ListBox to the View
     <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
    <Grid.RowDefinitions>
    <RowDefinition Height="25*"/>
    <RowDefinition Height="231*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="205*"/>
    <ColumnDefinition Width="478*"/>
    </Grid.ColumnDefinitions> <ListBox x:Name="ListPerson" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Contacts}" 
        SelectedItem="{Binding SelectedContact, Mode=TwoWay}" DisplayMemberPath="Name" FontSize="24" >
      </ListBox>

    Checkout that the ItemsSource is set to Contacts (ObservableCollection), SelectedItem is set to our SelectedItem property. Its a two way binding and we are showing only Name of our contact using DisplayMemberPath property.

  3. Finally we add the Form using a GridView
    <ListBox x:Name="ListPerson" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Contacts}" 
    SelectedItem="{Binding SelectedContact, Mode=TwoWay}" DisplayMemberPath="Name" FontSize="24" >
    </ListBox> <Grid x:Name="ContactLst" DataContext="{Binding SelectedContact}" Grid.Row="1" Grid.Column="1">
      <StackPanel Orientation="Vertical">
      <StackPanel>
      <TextBlock Text="Name: " FontSize="24"/>
      <TextBox Text="{Binding Name, Mode=TwoWay}" FontSize="24" Padding="10" Margin="10"/>
      </StackPanel>
      <StackPanel>
      <TextBlock Text="Email: " FontSize="24"/>
      <TextBox Text="{Binding Email, Mode=TwoWay}" FontSize="24" Padding="10" Margin="10"/>
      </StackPanel>
      </StackPanel>
      </Grid>

    We've set the DataContext of the Grid to "SelectedContact". Inside the Grid, we've got the stackpanel containing textboxes for Name and Email. Both of them are set to the respective properties. Again the binding is TwoWay.

  4. So what happens here in the View is, when we hit Refresh button, a list of contacts is populated in the ListBox. When we select any contact from ListBox, the "SelectedContact" property is set which in turn causes the TextBoxes to display the Name and Email of the "SelectedContact".
  5. If any thing changes in TextBoxes and we hit save/update/delete, then again our "SelectedContact" property is set with the new values and then the appropriate command is fired.

This is it! Finally our app is done. Give it a run. Try adding new values, updating existing ones and deleting a few. This should give a good start to anyone who is wandering blindly in the vast world of MVVM and Metro.

Files are attached with this post. Download and enjoy.

Cheers!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here