Silverlight RIA Tasks 2: Dynamic View Models






4.89/5 (25 votes)
Creating multiple dynamic Views using View Model Style and the Silverlight Tab Control.
Live example: Click here
Also see
- RIATasks: A Simple Silverlight CRUD Example
- Communication Between Master and Child View Models
- Central Silverlight Business Rules Validation
Dynamically Creating Views
When using "View Model Style" programming, you may find a need to dynamically create Views. The challenge is to create them dynamically, while also allowing a Designer to easily design the UI.
View Model Style
View Model Style allows a programmer to create an application that has absolutely no UI (user interface). The programmer only creates a View Model and a Model. A designer with no programming ability at all is then able to start with a blank page and completely create the View (UI) in Microsoft Expression Blend 4 (or higher).
If you are new to View Model Style, it is suggested that you read Silverlight View Model Style: An (Overly) Simplified Explanation for an introduction.
RIA Tasks 2
This article uses much of the same code used in RIATasks: A Simple Silverlight CRUD Example.
While this article uses the same database and website code, it covers these additional things:
- Dynamically creating Views (with View Models)
- Using the Silverlight Tab Control
- Creating a Design-time View for a dynamically created View Model
- Programmatically setting the selected Tab
- Programmatically creating TabItems and binding the Tab Control to them
- Programmatically setting the Style of dynamically created Tabs to a Static Resource
The Application
The previous application (RIATasks: A Simple Silverlight CRUD Example) only allows you to edit one Task at a time.
This application allows you to create unlimited Tabs, that each contain an editable Task.
The Web Service
While we normally use a View Model so that we don't need to change the UI when the design changes, if the basic requirements change, we usually need to actually change the code. In this case, the requirements have changed.
For the Web Site, the only code that needed to be changed was the Web Service code. The GetsTask
method was removed, and the GetTasks
method was altered to return the Task Description.
[WebService(Namespace = "http://OpenLightGroup.net/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class WebService : System.Web.Services.WebService
{
#region GetCurrentUserID
private int GetCurrentUserID()
{
int intUserID = -1;
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
// Get the current user
intUserID = Convert.ToInt32(HttpContext.Current.User.Identity.Name);
}
return intUserID;
}
#endregion
// Web Methods
#region GetTasks
[WebMethod]
public List<Task> GetTasks()
{
// Create a collection to hold the results
List<Task> colResult = new List<Task>();
RIATasksDBDataContext DB = new RIATasksDBDataContext();
var colTasks = from Tasks in DB.Tasks
where Tasks.UserID == GetCurrentUserID()
select Tasks;
return colTasks.ToList();
}
#endregion
#region DeleteTask
[WebMethod]
public string DeleteTask(int TaskID)
{
string strError = "";
RIATasksDBDataContext DB = new RIATasksDBDataContext();
try
{
var result = (from Tasks in DB.Tasks
where Tasks.TaskID == TaskID
where Tasks.UserID == GetCurrentUserID()
select Tasks).FirstOrDefault();
if (result != null)
{
DB.Tasks.DeleteOnSubmit(result);
DB.SubmitChanges();
}
}
catch (Exception ex)
{
strError = ex.Message;
}
return strError;
}
#endregion
#region UpdateTask
[WebMethod]
public string UpdateTask(Task objTask)
{
string strError = "";
RIATasksDBDataContext DB = new RIATasksDBDataContext();
try
{
var result = (from Tasks in DB.Tasks
where Tasks.TaskID == objTask.TaskID
where Tasks.UserID == GetCurrentUserID()
select Tasks).FirstOrDefault();
if (result != null)
{
result.TaskDescription = objTask.TaskDescription;
result.TaskName = objTask.TaskName;
DB.SubmitChanges();
}
}
catch (Exception ex)
{
strError = ex.Message;
}
return strError;
}
#endregion
#region InsertTask
[WebMethod]
public Task InsertTask(Task objTask)
{
RIATasksDBDataContext DB = new RIATasksDBDataContext();
try
{
Task InsertTask = new Task();
InsertTask.TaskDescription = objTask.TaskDescription;
InsertTask.TaskName = objTask.TaskName;
InsertTask.UserID = GetCurrentUserID();
DB.Tasks.InsertOnSubmit(InsertTask);
DB.SubmitChanges();
// Set the TaskID
objTask.TaskID = InsertTask.TaskID;
}
catch (Exception ex)
{
// Log the error
objTask.TaskID = -1;
objTask.TaskDescription = ex.Message;
}
return objTask;
}
#endregion
}
The Model
The Model was altered to not use Rx Extensions. While the code works either way, I was shown code by Richard Waddell and Shawn Wildermuth that requires about the same amount of code as Rx Extensions but does not require you at add any additional assemblies:
public class TasksModel
{
#region GetTasks
public static void GetTasks(EventHandler<GetTasksCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetTasksCompleted += eh;
WS.GetTasksAsync();
}
#endregion
#region DeleteTask
public static void DeleteTask(int TaskID,
EventHandler<DeleteTaskCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.DeleteTaskCompleted += eh;
WS.DeleteTaskAsync(TaskID);
}
#endregion
#region UpdateTask
public static void UpdateTask(Task objTask,
EventHandler<UpdateTaskCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.UpdateTaskCompleted += eh;
WS.UpdateTaskAsync(objTask);
}
#endregion
#region InsertTask
public static void InsertTask(Task objTask,
EventHandler<InsertTaskCompletedEventArgs> eh)
{
// Set up web service call
WebServiceSoapClient WS = new WebServiceSoapClient();
// Set the EndpointAddress
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.InsertTaskCompleted += eh;
WS.InsertTaskAsync(objTask);
}
#endregion
// Utility
#region GetBaseAddress
private static Uri GetBaseAddress()
{
// Get the web address of the .xap that launched this application
string strBaseWebAddress = App.Current.Host.Source.AbsoluteUri;
// Find the position of the ClientBin directory
int PositionOfClientBin =
App.Current.Host.Source.AbsoluteUri.ToLower().IndexOf(@"/clientbin");
// Strip off everything after the ClientBin directory
strBaseWebAddress = Strings.Left(strBaseWebAddress, PositionOfClientBin);
// Create a URI
Uri UriWebService =
new Uri(String.Format(@"{0}/WebService.asmx", strBaseWebAddress));
// Return the base address
return UriWebService;
}
#endregion
}
The View Model
This is where a lot of changes were made. Instead of one View Model, we now have three (we also have three Views).
Each View Model handles a different part of the application. Here is the overview:
- MainPageModel.cs - This is the View Model for the main View that loads all the other embedded Views. The Add New Task button is on the View of this View Model, but it calls an
ICommand
that is in theTabControlModel
's View Model (using View Model to View Model Communication described in this article: Silverlight View Model Communication). - TabControlModel.cs - This View Model dynamically creates
TabItem
s and places an instance of the TaskDetails View, and its View Model (TaskDetailsModel.cs) on theTabItem
. - TaskDetailsModel.cs - This View Model holds the details of a single Task.
MainPageModel
This class does not contain a lot of code. It mostly contains a property (TabControlVM
) that will hold an instance of the TabControlModel
.
This allows the MainPage
View to call methods in the TabControlModel
through its View Model (MainPageModel
). This View Model to View Model Communication technique is covered in this article: Silverlight View Model Communication.
Here is the full code:
using System;
using System.ComponentModel;
namespace RIATasks
{
public class MainPageModel : INotifyPropertyChanged
{
public MainPageModel()
{
}
// Properties
#region TabControlVM
private TabControlModel _TabControlVM = new TabControlModel();
public TabControlModel TabControlVM
{
get { return _TabControlVM; }
private set
{
if (TabControlVM == value)
{
return;
}
_TabControlVM = value;
this.NotifyPropertyChanged("TabControlVM");
}
}
#endregion
// Utility
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
}
TabControlModel
This class performs 90% of the work of the application.
The main purpose of this View Model is to expose the collection of Tasks to the View. In the original RIA Tasks application, this collection was ObservableCollection<Task>
, but, for RIA Tasks 2 we decided to use the Tab Control and expose a collection of TabItem
s (ObservableCollection<TabItem>
):
#region colTabItems
private ObservableCollection<TabItem> _colTabItems
= new ObservableCollection<TabItem>();
public ObservableCollection<TabItem> colTabItems
{
get { return _colTabItems; }
private set
{
if (colTabItems == value)
{
return;
}
_colTabItems = value;
this.NotifyPropertyChanged("colTabItems");
}
}
#endregion
To fill the collection of TabItem
s, we use the GetTasks()
method that calls the Model and retrieves the Tasks for the currently logged in user:
#region GetTasks
private void GetTasks()
{
// Clear the current Tasks
colTabItems.Clear();
// Call the Model to get the collection of Tasks
TasksModel.GetTasks((Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
// loop thru each item
foreach (var objTask in EventArgs.Result)
{
// Create a TaskItem from the Task
TabItem objTabItem = CreateTaskItem(objTask);
// Add it to the Collection of TabItems
colTabItems.Add(objTabItem);
}
// Count the records returned
if (colTabItems.Count == 0)
{
// If there are no records, indicate that
Message = "No Records Found";
}
else
{
Message = "";
}
// If there is a CurrentTaskID set that
// as the Current task
if (CurrentTaskID != -1)
{
// Locate the Task
var objTabItem = (from TaskItem in colTabItems
let VM = (TaskItem.Content as TaskDetails).DataContext
where (VM as TaskDetailsModel).CurrentTask.TaskID == CurrentTaskID
select TaskItem).FirstOrDefault();
if (objTabItem != null)
{
// Set the CurrentTask as selected
objTabItem.IsSelected = true;
}
}
}
});
}
#endregion
The constructor for the View Model calls the GetTasks()
method when the View is loaded:
public TabControlModel()
{
AddNewTaskCommand = new DelegateCommand(AddNewTask, CanAddNewTask);
DeleteTaskCommand = new DelegateCommand(DeleteTask, CanDeleteTask);
UpdateTaskCommand = new DelegateCommand(UpdateTask, CanUpdateTask);
// The following line prevents Expression Blend
// from showing an error when in design mode
if (!DesignerProperties.IsInDesignTool)
{
// Get the Tasks for the current user
GetTasks();
}
}
The constructor also sets up the ICommand
s that are used to Add, Update and Delete Tasks.
Here is the code for the ICommand
s:
#region AddNewTaskCommand
public ICommand AddNewTaskCommand { get; set; }
public void AddNewTask(object param)
{
SetToNewTask();
}
private bool CanAddNewTask(object param)
{
// Only allow a New Task to be created
// If there are no other [New] Tasks
var colNewTasks = from Tasks in colTabItems
where (Tasks.Header as string).Contains("[New]")
select Tasks;
return (colNewTasks.Count() == 0);
}
#endregion
#region DeleteTaskCommand
public ICommand DeleteTaskCommand { get; set; }
public void DeleteTask(object param)
{
// Get The Task
Task objTask = GetTaskFromTaskDetails((param as TaskDetails));
if (objTask.TaskID != -1)
{
// Delete Task
DeleteTask(objTask);
}
else
{
RemoveTask(objTask.TaskID);
}
}
private bool CanDeleteTask(object param)
{
// Only allow this ICommand to fire
// if a TaskDetails was passed as a parameter
return ((param as TaskDetails) != null);
}
#endregion
#region UpdateTaskCommand
public ICommand UpdateTaskCommand { get; set; }
public void UpdateTask(object param)
{
// Get The Task
Task objTask = GetTaskFromTaskDetails((param as TaskDetails));
if (objTask.TaskID == -1)
{
// This is a new Task
InsertTask(objTask);
}
else
{
// This is an Update
UpdateTask(objTask);
}
}
private bool CanUpdateTask(object param)
{
// Only allow this ICommand to fire
// if a TaskDetails was passed as a parameter
return ((param as TaskDetails) != null);
}
#endregion
These Commands use the following methods to call the Model and perform their operations:
#region DeleteTask
private void DeleteTask(Task objTask)
{
// Call the Model to delete the Task
TasksModel.DeleteTask(objTask.TaskID, (Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Set the Error Property
Message = EventArgs.Result;
RemoveTask(objTask.TaskID);
}
});
}
#endregion
#region UpdateTask
private void UpdateTask(Task objTask)
{
// Call the Model to UpdateTask the Task
TasksModel.UpdateTask(objTask, (Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Set the Error Property
Message = EventArgs.Result;
}
});
}
#endregion
#region InsertTask
private void InsertTask(Task objTask)
{
// Call the Model to Insert the Task
TasksModel.InsertTask(objTask, (Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
// Set the CurrentTaskID Property
// So it can be selected when Tasks re-load
CurrentTaskID = EventArgs.Result.TaskID;
// Update the Tasks list
GetTasks();
Message = "";
}
});
}
#endregion
You will notice that the AddNewTask
method calls the SetToNewTask()
method:
#region SetToNewTask
private void SetToNewTask()
{
// Unset selected for all Items
foreach (var item in colTabItems)
{
item.IsSelected = false;
}
// Create a empty Task
// so form will be blank
Task objTask = new Task();
// Set TaskID = -1 so we know it's a new Task
objTask.TaskID = -1;
// Create a TaskItem from the Task
TabItem objNewTabItem = CreateTaskItem(objTask);
// Set it as selected
objNewTabItem.IsSelected = true;
// Add it to the Collection of TabItems
this.colTabItems.Add(objNewTabItem);
}
#endregion
Note: This method first sets all TabItem
s' IsSelected
to false
, then sets the new TabItem
's IsSelected
to true
. This is how you make a Tab Control programmatically select a tab.
The SetToNewTask()
method calls the CreateTaskItem(objTask)
method (shown below) that converts a Task
to TabItem
. The SetToNewTask()
method then adds the TabItem
to the colTabItems
collection (the Tab Control on the View is bound to this collection so that it can show the tabs):
#region CreateTaskItem
private TabItem CreateTaskItem(Task Task)
{
// Create a Tasks Details
TaskDetails objTaskDetails = new TaskDetails();
// Get it's DataContext
TaskDetailsModel objTaskDetailsModel =
(TaskDetailsModel)objTaskDetails.DataContext;
// Call a method to set the Current Task at it's DataContext
objTaskDetailsModel.SetCurrentTask(Task);
// Create a TabItem
TabItem objTabItem = new TabItem();
// Give it a name so it can be programatically manipulated
objTabItem.Name = string.Format("DynamicTab_{0}", Task.TaskID.ToString());
// Set the Style to point to a Resource that the Designer
// can later change
objTabItem.Style = (Style)App.Current.Resources["TabItemStyle1"];
// Set it's Header
string strTaskID = (Task.TaskID == -1) ? "[New]" : Task.TaskID.ToString();
objTabItem.Header = String.Format("Task {0}", strTaskID);
// Set it's Content to the Tasks Details control
objTabItem.Content = objTaskDetails;
return objTabItem;
}
#endregion
- The
CreateTaskItem(objTask)
method creates an instance of the View,TaskDetails
, and an instance of its View Model,TaskDetailsModel
, and sets the currentTask
for it to the selectedTask
(using theSetCurrentTask(Task)
method). - It then places this View on a dynamically created
TabItem
(it sets the View as theContent
of theTabItem
). - It then returns the
TabItem
so that theSetToNewTask()
method can add it to thecolTabItems
collection.
Note: the line objTabItem.Style = (Style)App.Current.Resources["TabItemStyle1"];
is used to allow a Designer to alter the style of this dynamically created TabItem
by changing the style for the key TabItemStyle1
. In the RiaTasks2.zip project, that style is in the "RIATasks\Assets\TabControl.xaml" file.
TaskDetailsModel
The TaskDetailsModel
View Model is very simple. It contains a property to hold the current Task
and a method that allows it to be set:
public class TaskDetailsModel : INotifyPropertyChanged
{
public TaskDetailsModel()
{
}
public void SetCurrentTask(Task param)
{
CurrentTask = param;
}
#region CurrentTask
private Task _CurrentTask = new Task();
public Task CurrentTask
{
get { return _CurrentTask; }
private set
{
if (CurrentTask == value)
{
return;
}
_CurrentTask = value;
this.NotifyPropertyChanged("CurrentTask");
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
Note: The reason this class contains a method to set the Task
property is that the View that is bound to the Task
property will not update if you dynamically create the View and attempt to set the Task
property directly.
The View
We will now create the Views.
Here is the overview:
- MainPage.xaml - The View that loads all the other Views.
- TabControl.xaml - The View that contains the Tab Control and displays the Tabs.
- TaskDetails.xaml - The View that displays a single Task in a Tab.
MainPage View
MainPage
contains an Add New Tasks button.
We drop an InvokeCommandAction
behavior on the button.
When we Data bind the Command
parameter of the behavior...
...we see that we are able to bind the ICommand
that is in the TabControlModel
View Model (that is stored in the TabControlVM
property).
We can then go to Assets and drag and drop the TabControl
View Model onto the page...
...and bind its DataContext
to the TabControlVM
property (this enables View Model to View Model Communication described in this article: Silverlight View Model Communication).
TabControl View
While the View Model for this View does most of the work for the application, the View is actually very straightforward.
The buttons are bound to the appropriate ICommand
s (using InvokeCommandAction
behaviors), and the Tab Control is bound to the colTabItems
collection.
It is important to note that the Update and Delete buttons, pass as a parameter, the currently selected TabItem
(actually, they pass the Content
of the TabItem
which is the TaskDetails
View and View Model).
This is how the methods know what Task to Update or Delete.
TaskDetails View
The binding for this View is also really simple.
However, this demonstrates the power of View Models:
- The
TabControlModel
creates aTask
and binds it to a dynamically created instance of theTaskDetailsModel
- It then places it in a collection of
TabItem
s and the Tab Control is bound to it - To Update or Delete the
Task
, the View is simply passed as a parameter to the appropriate method
Alternate Styles
The primary reason for using a View Model, besides that it is usually less code than using the code-behind style, is that you decouple the View from the code and allow a designer to create and re-create the View without altering any code.
Alan Beasley provided a style for the tabs and the buttons. The Tabs
style styles the Tabs in all four positions (RIATasks2ABVersion.zip). To use it, we only needed to alter the resource files in the Assets directory. We have also specified the Tab Control to display the Tabs on the left side. This property is a standard property of the Tab Control.
Haruhiro Isowa created a Tab Control (RiaTasks2Hiro.zip) that displays all the Tabs on one scrollable row. He did have to create code to make his Tab Control scrollable, but the RIA Tasks 2 code was not altered at all (other than the .xaml files that the Designer would normally modify).
View Model - Not Hard At All
Hopefully you can see that View Model is not hard at all. It really is not complicated once you see how it is done. Expression Blend was designed to work in "View Model Style", so you should have an easier time using Expression Blend when you use this simple pattern.
We also demonstrated View Model Communication that you will hopefully find easy to understand. In addition, we covered dynamically creating Views while allowing a Designer full easy access to completely change the look of the application.