Accessing Data Using Silverlight
- Work with XML data in Silverlight
- Store data to and retrieve data from isolated storage using Silverlight
This lesson will introduce the how to store and retrieve data using Silverlight.
Storing Data in Code
When working with data programmatically, data is typically stored in memory for later reference, while other operations are carried out, or while the data stored in memory is manipulated. Data may be entered by a user, retrieved from a data source, or created programmatically.
Variables
The most basic means of storing data in memory is through the use of variables. All programming languages support some form of variables. A variable is a named location in memory used to store data. The specifics of how data is stored and managed in memory is particular to a programming language. Strongly-typed programming languages are very strict about what type of data can be stored in a variable. Weakly-typed programming languages are not strict about what type of data can be stored in a variable. Strongly-typed programming languages are generally more efficient than weakly-typed programming languages.
Silverlight may be coded using several languages. Some of the languages used in Silverlight, such as JavaScript, are weakly-typed, while others, such as C#, are strongly-typed. A developer must thoroughly understand how to work with variables in the language that they choose for working with Silverlight. The code snippet below illustrates declaration and initialization of a simple integer type variable in C# named x.
// simple variable. int x = 1;
Collections
Most programming languages support not only storing a single piece of data in a named location in memory but also support storing multiple pieces of data in a named location in memory. A collection is a named location in memory, similar to a variable, that is structured for storing multiple pieces of data. Depending upon the programming language used, multiple types of collections may be supported. For example, C# supports simple arrays, ArrayLists, Stacks, Queues, and HashTables. Each type of collection supported is structured for storing and retrieving data in a different fashion.
Collections are located in the System.Collections namespace. The code snippet below illustrates declaration and initialization of a standard string array named customerNames.
// store customer information in an ArrayList. string[]
customerNames = new string[3]; customerNames[0] = "Shannon Horn";
customerNames[1] = "Benny Madrid"; customerNames[2] = "Edwin Dewees";
Generics
Collections are versatile and simplify data storage in code by making it easier to store and transport multiple data items and objects. However, standard collections have some drawbacks as well. A standard collection, including an ArrayList, Stack, Queue, and HashTable, stores data internally as a simple object. By storing data as a simple object, a standard collection can be used to store any type of data. In a nutshell, in C#, a standard collection is a weakly-typed construct that exists in a strongly-typed language. The internal storage design of a standard collection affects performance and type safety negatively.
When a data item is stored in a standard collection, it must be converted to a simple object type. When a data item stored in a standard collection is removed from the collection, it must be converted from a simple object type to the destination type. The process of converting data to and from a simple object degrades performance. Additionally, a simple object can be converted to any more complex type. However, there is no guarantee that the data stored as a simple object will be correctly represented when removed from the standard collection and converted to a more complex type. For example, a complex object that represents data about a customer may be stored in a collection and then removed from the collection and converted to a string. The code written to perform the operation should compile but will, more than likely, cause errors to occur at runtime. hence, type safety is lost.
In the .NET Framework, generic collections are located in the System.Collections.Generic namespace. A generic collection is a strongly-typed collection and requires that the data type to be used for storage be specified at the time the collection is instantiated.
// a generic collection for storing customer names as
strings. List<string> customerNames = new List<string>(3);
customerNames.Add("Shannon Horn"); customerNames.Add("Benny Madrid");
customerNames.Add("Edwin Dewees");
In the code snippet above, the customerNames generic collection will only store string values however the collection could be configured to store and manage any valid .NET type.
Working with XML
The Extensible Markup Language (XML) was released by the World Wide Web Consortium (W3C - http://www.w3.org) in 1999 as a standardized means of storing and transporting data over the Web. XML has proliferated Web development technologies and virtually all software development platforms support some form of XML interaction. The .NET Framework contains a gamut of classes for working with XML data in the System.Xml namespace.
Silverlight contains a subset of XML functionality in the System.Xml namespace. XML data can be read using the XmlReader class and XML data can be written using the XmlWriter class. Additionally, Silverlight includes a class used to specify configuration settings to be used when writing XML data, the XmlWriterSettings class. If configuration settings are not specified using the XmlWriterSettings class, default configuration settings are used. In the code listing below, the XmlReader class, the XmlWriter class, and the XmlWriterSettings class are used to read in a well-formed XML string, parse it, and write the contents of it to a TextBlock.
using System; using System.Collections.Generic; using
System.Linq; using System.Net; using System.Windows; using
System.Windows.Controls; using System.Windows.Documents; using
System.Windows.Input; using System.Windows.Media; using
System.Windows.Media.Animation; using System.Windows.Shapes; using System.Xml;
using System.Text; using System.IO; namespace ADXmlReader { public partial class
Page : UserControl { public Page() { InitializeComponent(); } private void
UserControl_Loaded(object sender, RoutedEventArgs e) { // store names as XML.
string names = "<?xml version='1.0' encoding='utf-8' ?><Names><Name>Shannon
Horn</Name><Name>Benny Madrid</Name><Name>Edwin Dewees</Name></Names>"; //
create a reader. XmlReader reader = XmlReader.Create(new StringReader(names));
XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true;
settings.ConformanceLevel = ConformanceLevel.Auto; StringBuilder output = new
StringBuilder(); XmlWriter writer = XmlWriter.Create(output, settings); //
display the names. while (reader.Read()) { if (reader.NodeType ==
XmlNodeType.Text) { writer.WriteString(reader.Value + Environment.NewLine); } }
reader.Close(); writer.Close(); tbNames.Text = output.ToString(); } } }
The results of the code listing above are shown in the figure below.
Language Integrated Query (LINQ)
A major addition to the .NET Framework in version 3.5 is Language Integrated Query (LINQ). Most seasoned developers have mastered or are adequately familiar with the Structured Query Language (SQL). SQL is used to query relational database data. However, in many cases, SQL queries that pull data from a relational database schema are abstracted away from business logic and middle-tier code.
Data may also be stored in formats other than a relational database such as an XML file or a consumed Web service. In each data storage scenario, typically, a specialized language is used to retrieve and query the contained data. Furthermore, data is generally represented at the business logic and code level through objects, arrays, and collections. Developers regularly have to search these constructs by using tailor-made loops.
Many programmers have long requested a language for querying data stored in programming constructs and object oriented mechanisms. SQL is a stable and well-entrenched industry standard. It would be an insurmountable task to attempt to extend SQL so that it could be used to query programming constructs and other data sources. However, Microsoft was determined to make things easier for programmers by creating a standard for querying data stored in multiple data storage mechanisms and coding constructs. The result of their efforts was a new query language that targets data stored in objects and collections, Language Integrated Query (LINQ). LINQ was also extended to be able to query relational data stored in databases, XML data, and other data sources. However, data queried by using LINQ must be stored as objects. If data is queried from a relational data source using LINQ, it must first be represented using an object model. (see footnote)
LINQ is capable of querying any object programmatically that implements the IEnumerable interface. LINQ will present an entirely new programming paradigm to experienced .NET developers but the new functionality and benefits thereof should be quickly enjoyed and adapted by most. To summarize, the primary benefits of using LINQ are a single, consistent language for querying data across any type of data source and a means of doing so that is type safe and supported by the most popular .NET Framework programming languages.
LINQ has grown into an extensive query language. Additionally, in order to make LINQ relevant and a valid solution into the future, Microsoft designed LINQ to be extensible so that it can be extended by Microsoft or third party vendors to support additional data sources. Comprehensive coverage of LINQ is beyond the scope of this course. However, as an example, we will create a simple LINQ query example here using Silverlight. Silverlight supports LINQ using classes in the System.Linq namespace.
The first step in working with LINQ is to identify a data source. In the example created here, we store a list of names in a simple string array. The second step in working with LINQ is to create the LINQ query. A LINQ query uses very similar concepts and vocabulary as an SQL query, however the clauses are presented in a different order. Finally, the third step in working with LINQ is to execute the query. A LINQ query is executed using a foreach loop in C#. The code snippet below illustrates a simple LINQ query against a list of names in a string array. The LINQ query below uses the where clause to filter out all names except those that begin with the letter "E".
// obtain the data source. // list of names. string[]
namesList = new string[3] { "Shannon Horn","Benny Madrid","Edwin Dewees"}; //
create the query. var names = from name in namesList where name.Substring(0, 1)
== "E" select name; // execute the query. foreach (string name in names) {
tbOutput.Text = name; }
The results of the code snippet above are shown in the figure below.
For more information about Language Integrated Query (LINQ), visit the MSDN article entitled Language Integrated Query (LINQ) located at http://msdn.microsoft.com/en-us/library/bb397926.aspx.
Isolated Storage
Due to the security constraints placed upon a Silverlight application (the "sandbox" that it operates in), a Silverlight application cannot write directly to or read directly from the file system on the client's machine. In an effort to allow developers to store some data local to the client, Microsoft designed Silverlight to read data from and write data to a virtual file system called Isolated Storage. Isolated storage is stored inside a User's Application Data directory:
Location of Isolated Storage in Vista
C:\Users\AppData\LocalLow\Microsoft\Silverlight\is
Location of Isolated Storage in Windows XP
C:\Documents and Settings\Local Settings\Application Data\Microsoft\Silverlight\is
Silverlight isolated storage is currently limited to a 100 KB capacity and the classes used to work with isolated storage are located in the System.IO.IsolatedStorage namespace. Isolated Storage is Non-Volatile, and is not cleared by actions such as when the user clears the browser cache or deletes cookies. The code snippet below illustrates saving a user's login credentials to isolated storage in a file named UserCredentials.txt.
// remember the user's credentials for next time. if
(chkRememberMe.IsChecked == true) { using (IsolatedStorageFile isoStore =
IsolatedStorageFile.GetUserStoreForApplication()) { using
(IsolatedStorageFileStream isoStream = new
IsolatedStorageFileStream("UserCredentials.txt", FileMode.Create, isoStore)) {
using (StreamWriter writer = new StreamWriter(isoStream)) {
writer.Write(user.UserName + "|" + user.PasswordHash); } } } }
The code snippet above illustrates using the IsolatedStorageFile class and the IsolatedStorageFileStream class to work with isolated storage. The code snippet below illustrates using the same classes to determine if the UserCredentials.txt file exists in isolated storage and, if it does exist, reads the contents of the file.
// determine if the user's credentials exist in isolated
storage. using (IsolatedStorageFile isoStore =
IsolatedStorageFile.GetUserStoreForApplication()) { using
(IsolatedStorageFileStream isoStream = new
IsolatedStorageFileStream("UserCredentials.txt", FileMode.Open, isoStore)) {
using (StreamReader reader = new StreamReader(isoStream)) { // read the
credentials. string[] sb = reader.ReadLine().Split('|'); // do we have
credentials? if (sb.Length > 0) { // if the credentials exist, parse them out
and authenticate them. user.UserName = sb[0]; user.Password = sb[1]; //
authenticate. svc.AuthenticateUserAsync(user.UserName, user.Password); } } } }
Data Binding
Much of a database oriented application involves reading data from a database and presenting that data to the user. One way to handle filling User Interface elements with data from a database is to simply assign values through code. The code snippet below shows how this might be done...
// assume we have an "Athlete" class as follows: public class
AthleteDisplayInfo { public int AthleteId { get; set; } public string FirstName
{get; set;} public string LastName { get; set; } } // we then retrieve an
athlete from the database AthleteDisplayInfo athlete = e.Result; // we can then
fill UI elements, such as textboxes txtFirstName.Text = athlete.FirstName;
txtLastName.Text = athlete.LastName; // and also get back the values after the
user updates... athlete.FirstName = txtFirstName.Text; athlete.LastName =
txtLastName.Text;
The method shown above is quite adequate for filling UI elements with data values, but it does not allow for separation of User Interface from Business Logic Classes. Without separating our user interface from our business logic code, it will be more difficult to test the application, and have teams of designers and developers work cooperatively on the same project.
This is where data binding comes in. Using data binding, we can declaratively assign values to UI elements instead of using code. Furthermore, the data binding will automatically synchronize changes to the data source to the UI elements.To implement data binding, we use a special syntax in XAML, inside any attribute value: "{Binding PropertyName, Mode=OneWay}" - where "PropertyName" is the property of a data source class, and Mode is either OneTime, OneWay, or TwoWay.
Consider this example:
<TextBox x:Name="txtLastName" Text="{Binding LastName,
Mode=OneWay}" />
The XAML above will automatically fill the "Text" property of txtLastName when databinding occurs. We can tell UI elements what their binding source is by using the DataContext property. The DataContext property can be assigned to a Container Control, such as a Canvas or Grid, and all child controls within that container will receive their bound data from that DataContext. For example, if txtLastName from the example above exists within a Canvas container named "LayoutRoot", then we can assign a business logic class to LayoutRoot.DataContext:
LayoutRoot.DataContext = athlete;
The assignment to DataContext above would cause txtLastName to show the value of athlete.LastName in its Text property.
Data Binding Modes
When you are specifying the Mode for data binding, you have three choices:
- OneTime: Updates the target property when the binding is created.
- OneWay: Updates the target property when the binding is created. Changes to the source object can also propogate to the target.
- TwoWay: Updates either the target or the source object when either change. When the binding is created, the target property is updated from the source.
If you have a read-only UI element where the source data does not change, you might consider using OneTime mode data binding. If you have a read-only UI element, and the user may be selecting different records at times (causing the source data to change), you might consider using OneWay databinding. TwoWay databinding is handy in Master/Details scenarios, where a DataGrid can be linked to controls in a "Detail" section of the screen.
Accessing Data Using Silverlight Conclusion
Lab: Accessing Data In Silverlight
In this lab, you will extend the athlete management application login dialog by providing the user the option to save their credentials and automatically login on future visits. The user credentials will be stored in isolated storage.
Store User Credentials in Isolated Storage
In this exercise, you will store user credentials in isolated storage.
- Let's improve the login dialog by adding a checkbox to the canvas so that users can select an option to automatically log them in on follow-up visits. We'll implement this by storing the user's login credentials in isolated storage. Bear in mind that isolated storage is not guaranteed to be persistent. Isolated storage presents a virtual file system to the developer by using cookies. If a user deletes the associated cookies, they will remove their login information and will be required to login again on following visits (just as with any Web site).
- We will need to add references to the System.IO and System.IO.IsolatedStorage
namespace. Add this to the top of the Page.xaml.cs code file:
using System.IO.IsolatedStorage; using System.IO;
- The AuthenticateUserCompleted event is the place we'll want to store the user's
information in isolated storage if they select the checkbox for us to do so.
That way we don't forget to store the credentials away at a later point. When
writing to isolated storage, you can gain access to the isolated storage
mechanism through the System.IO.IsolatedStorage.IsolatedStorageFile class. Once
an instance of the file class is created, a stream must be created for reading
from and writing to the file. Finally, a file stream is used to actually write
into the stream. The example version of the updated AuthenticateUserCompleted
event is shown below:
void svc_AuthenticateUserCompleted(object sender, AthleteManager.AthleteService.AuthenticateUserCompletedEventArgs e) { if (e.Result) { ucLoginStatus1.IsLoggedIn = true; // remember the user's credentials for next time. if (chkRememberMe.IsChecked == true) { using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication()) { using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream("UserCredentials.txt", FileMode.Create, isoStore)) { using (StreamWriter writer = new StreamWriter(isoStream)) { writer.Write(txtUserName.Text + "|" + txtPassword.Password); } } } } } else { ucLoginStatus1.IsLoggedIn = false; } }
- Next, we'll need to read the user's credentials from isolated storage on follow up visits, if the information is stored in isolated storage. We'll want to completely subvert the login dialog in this scenario, if we can, so we'll add code to the code behind class constructor to check for the user's credentials in isolated storage.
- The process of reading from isolated storage is almost exactly the opposite to
the process of writing to isolated storage. The updated example code behind
constructor is shown below:
public Page() { InitializeComponent(); svc.AuthenticateUserCompleted += new EventHandler<AthleteManager.AthleteService.AuthenticateUserCompletedEventArgs>(svc_AuthenticateUserCompleted); // determine if the user's credentials exist in isolated storage. using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication()) { using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream("UserCredentials.txt", FileMode.Open, isoStore)) { using (StreamReader reader = new StreamReader(isoStream)) { // read the credentials. string[] sb = reader.ReadLine().Split('|'); // if the credentials exist, parse them out and authenticate them. string username = sb[0]; string password = sb[1]; // authenticate. svc.AuthenticateUserAsync(username, password); } } } btnLogin.Click += new RoutedEventHandler(btnLogin_Click); }
Add Controls to Display Athlete Information
In this exercise, you will add controls to the athlete management application design area to display athlete information.
- Let's enhance our web service so that it can read Athlete information from the
database. First, add a few namespace imports to the top of AthleteService.cs in
the web project: using System.Data.SqlClient; using System.Configuration; using System.Data;
- We need a Connection object to the database. Add this declaration just inside
the AthleteService class:SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["athleteDB"].ConnectionString);
- Next we add a new web service method to AthleteService.cs to retrieve the
athlete information:[WebMethod] public List<Athlete> ReadAthletes() { List<Athlete> athletes = new List<Athlete>(); cn.Open(); using (SqlCommand cmd = new SqlCommand("select * from Athletes", cn)) { cmd.CommandType = CommandType.Text; SqlDataReader rdr = cmd.ExecuteReader(); if (rdr.HasRows) { Athlete athlete; while (rdr.Read()) { athlete = new Athlete(); athlete.AthleteId = int.Parse(rdr["AthleteId"].ToString()); athlete.SportId = int.Parse(rdr["SportId"].ToString()); athlete.FirstName = rdr["FirstName"].ToString(); athlete.LastName = rdr["LastName"].ToString(); athlete.Address = rdr["Address"].ToString(); athlete.City = rdr["City"].ToString(); athlete.State = rdr["State"].ToString(); athlete.Zip = rdr["Zip"].ToString(); athletes.Add(athlete); } } } cn.Close(); cn.Dispose(); return athletes; }
- Since we have added a method to the Web Service, we need to refresh the Service
Reference from the Silverlight application. In the Silverlight project, expand
the Service References node and then right-click the AthleteService control and
select "Update Service Reference." Then, wire up the event handler for the call
to ReadAthletes on the web service. Place this code in the Page constructor,
just after the event wire-up for AuthenticateUserCompleted:
svc.ReadAthletesCompleted += new EventHandler<AthleteManager.AthleteService.ReadAthletesCompletedEventArgs>(svc_ReadAthletesCompleted);
- The next step of the process is to display the athletes that are in the database
in a datagrid. The datagrid is a control that is included in the Silverlight
SDK. It might be easiest to set the visibility of the login dialog to Collapsed
while designing the DataGrid and data display. Add code to the
svc_AuthenticateUserCompleted event handler to hide the login dialog if the user
has successfully authenticated, and call the ReadAthletesAsync method of the web
service:canvasLogin.Visibility = Visibility.Collapsed; svc.ReadAthletesAsync();
- Drag a DataGrid from the toolbox to the Silverlight XAML. Assign the DataGrid a
name and set the AutoGenerateColumns property to True.<my:DataGrid x:Name="grdAthletes" AutoGenerateColumns="True"></my:DataGrid>
- In the ReadAthletesCompleted callback event handler, write code to set the
results of the method as the DataGrid ItemSource. We are returning an array of
athlete objects from the ReadAthletes method. void svc_ReadAthletesCompleted(object sender, AthleteManager.AthleteService.ReadAthletesCompletedEventArgs e) { grdAthletes.ItemsSource = e.Result; }
- If you find that the DataGrid is not displaying data correctly, ensure that you specify Height and Width property values for the DataGrid. Test the Silverlight control to ensure that the DataGrid is displaying data correctly.
- Modify the Silverlight control by adding additional controls for displaying
athlete information, and buttons for Save, Add New and Delete. Use Expression
Blend to design this data entry form. First create a new Canvas named canvasMain
and be sure to place all of the following controls inside the Canvas (we will
later show/hide this Canvas as necessary). The figures below illustrate the
controls added to canvasMain and the resulting appearance.
- txtFirstName: A TextBox for first name.
- txtLastName: A TextBox for last name.
- txtAddress: A TextBox for address.
- txtCity: A TextBox for city.
- txtState: A TextBox for state.
- txtZip: A TextBox for zip.
- btnSave: A button to save the current record.
- btnAddNew: A button for entering "new record" mode.
- btnDelete: A button for deleting the current record.
- Complete the Silverlight control by adding additional controls to the control for displaying athlete information.
- Add Data Binding markup syntax to the textboxes in XAML so that they show the
value of the fields when databound:
<TextBox Height="20" x:Name="txtFirstName" Width="137" Canvas.Left="89" Canvas.Top="255" Text="{Binding FirstName, Mode=TwoWay}" TextWrapping="Wrap" /> <TextBox Height="20" x:Name="txtLastName" Width="137" Text="{Binding LastName, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Top="255" Canvas.Left="240"/> <TextBox Height="20" x:Name="txtAddress" Width="285" Text="{Binding Address, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Left="89" Canvas.Top="288"/> <TextBox Height="20" x:Name="txtCity" Width="99" Text="{Binding City, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Left="89" Canvas.Top="320"/> <TextBox Height="20" x:Name="txtState" Width="29.539" Text="{Binding State, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Left="240" Canvas.Top="320"/> <TextBox Height="20" x:Name="txtZip" Width="60" Text="{Binding Zip, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Top="320" Canvas.Left="314"/>
- Ensure that the new controls only display when the user has successfully logged
into the application. You can do this by adding code to the
svc_AuthenticateUserCompleted event:
canvasMain.Visibility = Visibility.Visible;
- Add an event handler to the DataGrid SelectionChange event. In the event
handler, set the DataContext to data bind the values displayed in the controls
to the athlete information for the currently selected athlete in the DataGrid
list. The example code is shown below.
void grdAthletes_SelectionChanged(object sender, SelectionChangedEventArgs e) { AthleteService.Athlete athlete = (AthleteService.Athlete)grdAthletes.SelectedItem; LayoutRoot.DataContext = athlete; }
- Run the application. You should be able to browse the available records.
- Next we'll add update capabilities to the Save, Delete and Add New buttons.
Inside the AthleteService.cs Web Service class, add the following two Web
Methods:
[WebMethod] public void SaveAthlete(Athlete athlete) { string sqlText = string.Empty; if (athlete.AthleteId > 0) sqlText = "update Athletes set FirstName=@FirstName, LastName=@LastName, Address=@Address, City=@City, State=@State, Zip=@Zip where AthleteId = @AthleteId"; else sqlText = "insert into Athletes (FirstName, LastName, Address, City, State, Zip) values (@FirstName, @LastName, @Address, @City, @State, @Zip)"; cn.Open(); using (SqlCommand cmd = new SqlCommand(sqlText, cn)) { cmd.Parameters.Add(new SqlParameter("@FirstName", athlete.FirstName)); cmd.Parameters.Add(new SqlParameter("@LastName", athlete.LastName)); cmd.Parameters.Add(new SqlParameter("@Address", athlete.Address)); cmd.Parameters.Add(new SqlParameter("@City", athlete.City)); cmd.Parameters.Add(new SqlParameter("@State", athlete.State)); cmd.Parameters.Add(new SqlParameter("@Zip", athlete.Zip)); cmd.Parameters.Add(new SqlParameter("@AthleteId", athlete.AthleteId)); cmd.CommandType = CommandType.Text; cmd.ExecuteNonQuery(); } cn.Close(); cn.Dispose(); } [WebMethod] public void DeleteAthlete(Athlete athlete) { string sqlText = "delete from Athletes where AthleteId = @AthleteId"; cn.Open(); using (SqlCommand cmd = new SqlCommand(sqlText, cn)) { cmd.CommandType = CommandType.Text; cmd.ExecuteNonQuery(); } cn.Close(); cn.Dispose(); }
- Build the application and then refresh the Service References in the Silverlight project again as you did in a previous step (right-click the AthleteService reference and select "Update Service Reference.")
- Inside Page.xaml.cs, inside the constructor, wire up the Completed event
handlers for the new Save and Delete methods:
svc.DeleteAthleteCompleted += new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(svc_DeleteAthleteCompleted); svc.SaveAthleteCompleted += new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(svc_SaveAthleteCompleted);
- Now wire up the click event handlers for our Save, Add New, and Delete buttons.
Add this to the Page.xaml.cs constructor code:
btnSave.Click += new RoutedEventHandler(btnSave_Click); btnAddNew.Click += new RoutedEventHandler(btnAddNew_Click); btnDelete.Click += new RoutedEventHandler(btnDelete_Click);
- Lastly, we can call the web methods inside the button handers.
void btnDelete_Click(object sender, RoutedEventArgs e) { AthleteService.Athlete athlete = (LayoutRoot.DataContext as AthleteService.Athlete); svc.DeleteAthleteAsync(athlete); } void btnAddNew_Click(object sender, RoutedEventArgs e) { AthleteService.Athlete athlete = new AthleteService.Athlete(); LayoutRoot.DataContext = athlete; } void btnSave_Click(object sender, RoutedEventArgs e) { AthleteService.Athlete athlete = (LayoutRoot.DataContext as AthleteService.Athlete); svc.SaveAthleteAsync(athlete); }
- Run the application, and try adding, updating and deleting a record.
Lab: Accessing Data In Silverlight
In this lab, you will extend the athlete management application login dialog by providing the user the option to save their credentials and automatically login on future visits. The user credentials will be stored in isolated storage.
Exercise: Store User Credentials in Isolated Storage
In this exercise, you will store user credentials in isolated storage.
- Let's improve the login dialog by adding a checkbox to the canvas so that users can select an option to automatically log them in on follow-up visits. We'll implement this by storing the user's login credentials in isolated storage. Bear in mind that isolated storage is not guaranteed to be persistent. Isolated storage presents a virtual file system to the developer by using cookies. If a user deletes the associated cookies, they will remove their login information and will be required to login again on following visits (just as with any Web site).
- We will need to add references to the System.IO and System.IO.IsolatedStorage
namespace. Add this to the top of the Page.xaml.cs code file:
using System.IO.IsolatedStorage; using System.IO;
- The AuthenticateUserCompleted event is the place we'll want to store the user's
information in isolated storage if they select the checkbox for us to do so.
That way we don't forget to store the credentials away at a later point. When
writing to isolated storage, you can gain access to the isolated storage
mechanism through the System.IO.IsolatedStorage.IsolatedStorageFile class. Once
an instance of the file class is created, a stream must be created for reading
from and writing to the file. Finally, a file stream is used to actually write
into the stream. The example version of the updated AuthenticateUserCompleted
event is shown below:
void svc_AuthenticateUserCompleted(object sender, AthleteManager.AthleteService.AuthenticateUserCompletedEventArgs e) { if (e.Result) { ucLoginStatus1.IsLoggedIn = true; // remember the user's credentials for next time. if (chkRememberMe.IsChecked == true) { using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication()) { using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream("UserCredentials.txt", FileMode.Create, isoStore)) { using (StreamWriter writer = new StreamWriter(isoStream)) { writer.Write(txtUserName.Text + "|" + txtPassword.Password); } } } } } else { ucLoginStatus1.IsLoggedIn = false; } } - Next, we'll need to read the user's credentials from isolated storage on follow up visits, if the information is stored in isolated storage. We'll want to completely subvert the login dialog in this scenario, if we can, so we'll add code to the code behind class constructor to check for the user's credentials in isolated storage.
- The process of reading from isolated storage is almost exactly the opposite to
the process of writing to isolated storage. The updated example code behind
constructor is shown below:
public Page() { InitializeComponent(); svc.AuthenticateUserCompleted += new EventHandler<AthleteManager.AthleteService.AuthenticateUserCompletedEventArgs>(svc_AuthenticateUserCompleted); // determine if the user's credentials exist in isolated storage. using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication()) { using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream("UserCredentials.txt", FileMode.Open, isoStore)) { using (StreamReader reader = new StreamReader(isoStream)) { // read the credentials. string[] sb = reader.ReadLine().Split('|'); // if the credentials exist, parse them out and authenticate them. string username = sb[0]; string password = sb[1]; // authenticate. svc.AuthenticateUserAsync(username, password); } } } btnLogin.Click += new RoutedEventHandler(btnLogin_Click); }
Exercise: Add Controls to Display Athlete Information
In this exercise, you will add controls to the athlete management application design area to display athlete information.
- Let's enhance our web service so that it can read Athlete information from the
database. First, add a few namespace imports to the top of AthleteService.cs in
the web project:
using System.Data.SqlClient; using System.Configuration; using System.Data; - We need a Connection object to the database. Add this declaration just inside
the AthleteService class:
SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["athleteDB"].ConnectionString); - Next we add a new web service method to AthleteService.cs to retrieve the
athlete information:
[WebMethod] public List<Athlete> ReadAthletes() { List<Athlete> athletes = new List<Athlete>(); cn.Open(); using (SqlCommand cmd = new SqlCommand("select * from Athletes", cn)) { cmd.CommandType = CommandType.Text; SqlDataReader rdr = cmd.ExecuteReader(); if (rdr.HasRows) { Athlete athlete; while (rdr.Read()) { athlete = new Athlete(); athlete.AthleteId = int.Parse(rdr["AthleteId"].ToString()); athlete.SportId = int.Parse(rdr["SportId"].ToString()); athlete.FirstName = rdr["FirstName"].ToString(); athlete.LastName = rdr["LastName"].ToString(); athlete.Address = rdr["Address"].ToString(); athlete.City = rdr["City"].ToString(); athlete.State = rdr["State"].ToString(); athlete.Zip = rdr["Zip"].ToString(); athletes.Add(athlete); } } } cn.Close(); cn.Dispose(); return athletes; } - Since we have added a method to the Web Service, we need to refresh the Service
Reference from the Silverlight application. In the Silverlight project, expand
the Service References node and then right-click the AthleteService control and
select "Update Service Reference." Then, wire up the event handler for the call
to ReadAthletes on the web service. Place this code in the Page constructor,
just after the event wire-up for AuthenticateUserCompleted:
svc.ReadAthletesCompleted += new EventHandler<AthleteManager.AthleteService.ReadAthletesCompletedEventArgs>(svc_ReadAthletesCompleted); - The next step of the process is to display the athletes that are in the database
in a datagrid. The datagrid is a control that is included in the Silverlight
SDK. It might be easiest to set the visibility of the login dialog to Collapsed
while designing the DataGrid and data display. Add code to the
svc_AuthenticateUserCompleted event handler to hide the login dialog if the user
has successfully authenticated, and call the ReadAthletesAsync method of the web
service:
canvasLogin.Visibility = Visibility.Collapsed; svc.ReadAthletesAsync(); - Drag a DataGrid from the toolbox to the Silverlight XAML. Assign the DataGrid a
name and set the AutoGenerateColumns property to True.
<my:DataGrid x:Name="grdAthletes" AutoGenerateColumns="True"></my:DataGrid> - In the ReadAthletesCompleted callback event handler, write code to set the
results of the method as the DataGrid ItemSource. We are returning an array of
athlete objects from the ReadAthletes method.
void svc_ReadAthletesCompleted(object sender, AthleteManager.AthleteService.ReadAthletesCompletedEventArgs e) { grdAthletes.ItemsSource = e.Result; } - If you find that the DataGrid is not displaying data correctly, ensure that you specify Height and Width property values for the DataGrid. Test the Silverlight control to ensure that the DataGrid is displaying data correctly.
- Modify the Silverlight control by adding additional controls for displaying
athlete information, and buttons for Save, Add New and Delete. Use Expression
Blend to design this data entry form. First create a new Canvas named canvasMain
and be sure to place all of the following controls inside the Canvas (we will
later show/hide this Canvas as necessary). The figures below illustrate the
controls added to canvasMain and the resulting appearance.
- txtFirstName: A TextBox for first name.
- txtLastName: A TextBox for last name.
- txtAddress: A TextBox for address.
- txtCity: A TextBox for city.
- txtState: A TextBox for state.
- txtZip: A TextBox for zip.
- btnSave: A button to save the current record.
- btnAddNew: A button for entering "new record" mode.
- btnDelete: A button for deleting the current record.
- Complete the Silverlight control by adding additional controls to the control for displaying athlete information.
- Add Data Binding markup syntax to the textboxes in XAML so that they show the
value of the fields when databound:
<TextBox Height="20" x:Name="txtFirstName" Width="137" Canvas.Left="89" Canvas.Top="255" Text="{Binding FirstName, Mode=TwoWay}" TextWrapping="Wrap" /> <TextBox Height="20" x:Name="txtLastName" Width="137" Text="{Binding LastName, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Top="255" Canvas.Left="240"/> <TextBox Height="20" x:Name="txtAddress" Width="285" Text="{Binding Address, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Left="89" Canvas.Top="288"/> <TextBox Height="20" x:Name="txtCity" Width="99" Text="{Binding City, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Left="89" Canvas.Top="320"/> <TextBox Height="20" x:Name="txtState" Width="29.539" Text="{Binding State, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Left="240" Canvas.Top="320"/> <TextBox Height="20" x:Name="txtZip" Width="60" Text="{Binding Zip, Mode=TwoWay}" TextWrapping="Wrap" Canvas.Top="320" Canvas.Left="314"/> - Ensure that the new controls only display when the user has successfully logged
into the application. You can do this by adding code to the
svc_AuthenticateUserCompleted event:
canvasMain.Visibility = Visibility.Visible;
- Add an event handler to the DataGrid SelectionChange event. In the event
handler, set the DataContext to data bind the values displayed in the controls
to the athlete information for the currently selected athlete in the DataGrid
list. The example code is shown below.
void grdAthletes_SelectionChanged(object sender, SelectionChangedEventArgs e) { AthleteService.Athlete athlete = (AthleteService.Athlete)grdAthletes.SelectedItem; LayoutRoot.DataContext = athlete; } - Run the application. You should be able to browse the available records.
- Next we'll add update capabilities to the Save, Delete and Add New buttons.
Inside the AthleteService.cs Web Service class, add the following two Web
Methods:
[WebMethod] public void SaveAthlete(Athlete athlete) { string sqlText = string.Empty; if (athlete.AthleteId > 0) sqlText = "update Athletes set FirstName=@FirstName, LastName=@LastName, Address=@Address, City=@City, State=@State, Zip=@Zip where AthleteId = @AthleteId"; else sqlText = "insert into Athletes (FirstName, LastName, Address, City, State, Zip) values (@FirstName, @LastName, @Address, @City, @State, @Zip)"; cn.Open(); using (SqlCommand cmd = new SqlCommand(sqlText, cn)) { cmd.Parameters.Add(new SqlParameter("@FirstName", athlete.FirstName)); cmd.Parameters.Add(new SqlParameter("@LastName", athlete.LastName)); cmd.Parameters.Add(new SqlParameter("@Address", athlete.Address)); cmd.Parameters.Add(new SqlParameter("@City", athlete.City)); cmd.Parameters.Add(new SqlParameter("@State", athlete.State)); cmd.Parameters.Add(new SqlParameter("@Zip", athlete.Zip)); cmd.Parameters.Add(new SqlParameter("@AthleteId", athlete.AthleteId)); cmd.CommandType = CommandType.Text; cmd.ExecuteNonQuery(); } cn.Close(); cn.Dispose(); } [WebMethod] public void DeleteAthlete(Athlete athlete) { string sqlText = "delete from Athletes where AthleteId = @AthleteId"; cn.Open(); using (SqlCommand cmd = new SqlCommand(sqlText, cn)) { cmd.CommandType = CommandType.Text; cmd.ExecuteNonQuery(); } cn.Close(); cn.Dispose(); } - Build the application and then refresh the Service References in the Silverlight project again as you did in a previous step (right-click the AthleteService reference and select "Update Service Reference.")
- Inside Page.xaml.cs, inside the constructor, wire up the Completed event
handlers for the new Save and Delete methods:
svc.DeleteAthleteCompleted += new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(svc_DeleteAthleteCompleted); svc.SaveAthleteCompleted += new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(svc_SaveAthleteCompleted); - Now wire up the click event handlers for our Save, Add New, and Delete buttons.
Add this to the Page.xaml.cs constructor code:
btnSave.Click += new RoutedEventHandler(btnSave_Click); btnAddNew.Click += new RoutedEventHandler(btnAddNew_Click); btnDelete.Click += new RoutedEventHandler(btnDelete_Click); - Lastly, we can call the web methods inside the button handers.
void btnDelete_Click(object sender, RoutedEventArgs e) { AthleteService.Athlete athlete = (LayoutRoot.DataContext as AthleteService.Athlete); svc.DeleteAthleteAsync(athlete); } void btnAddNew_Click(object sender, RoutedEventArgs e) { AthleteService.Athlete athlete = new AthleteService.Athlete(); LayoutRoot.DataContext = athlete; } void btnSave_Click(object sender, RoutedEventArgs e) { AthleteService.Athlete athlete = (LayoutRoot.DataContext as AthleteService.Athlete); svc.SaveAthleteAsync(athlete); } - Run the application, and try adding, updating and deleting a record.
In this lesson of the Silverlight tutorial, you
- Managed data by using LINQ
- Stored and retrieved XML using Silverlight
- Stored data to and retrieved data from isolated storage
Footnotes
-
Comprehensive coverage of LINQ is well beyond the scope of this course. To learn more about LINQ, visit the LINQ Developer Center (the LINQ Project) located at http://msdn2.microsoft.com/en-us/netframework/aa904594.aspx.