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:
="1.0" ="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.
- 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)
- 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; }}}
- 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>
- 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 });
}
}
}
}
- 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".
- 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);
}
}
}
- The insert, update and delete operations will not use
XDocument
.
Instead we will use the XmlDocument
and deal with various XmlElement
.
- 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);
}
}
}
- We use XPath to find and update our contact. You will need to brush up your XPaths!
- 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/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);
}
}
}
- 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(); 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.
- 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!");
}
- 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!");
}
- Next is the update method.
void OnUpdate(object obj)
{
db.Update(SelectedContact);
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Contacts"));Debug.WriteLine("Update Called!");
}
- Next comes the delete method.
void OnDelete(object obj)
{
db.Delete(SelectedContact);
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("SelectedContact")); Debug.WriteLine("Delete Called!");
}
- 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.
- 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>
- 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.
- 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.
- 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
".
- 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!