Live example: Click here
Also see
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)
{
intUserID = Convert.ToInt32(HttpContext.Current.User.Identity.Name);
}
return intUserID;
}
#endregion
#region GetTasks
[WebMethod]
public List<Task> GetTasks()
{
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();
objTask.TaskID = InsertTask.TaskID;
}
catch (Exception ex)
{
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)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.GetTasksCompleted += eh;
WS.GetTasksAsync();
}
#endregion
#region DeleteTask
public static void DeleteTask(int TaskID,
EventHandler<DeleteTaskCompletedEventArgs> eh)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.DeleteTaskCompleted += eh;
WS.DeleteTaskAsync(TaskID);
}
#endregion
#region UpdateTask
public static void UpdateTask(Task objTask,
EventHandler<UpdateTaskCompletedEventArgs> eh)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.UpdateTaskCompleted += eh;
WS.UpdateTaskAsync(objTask);
}
#endregion
#region InsertTask
public static void InsertTask(Task objTask,
EventHandler<InsertTaskCompletedEventArgs> eh)
{
WebServiceSoapClient WS = new WebServiceSoapClient();
WS.Endpoint.Address = new EndpointAddress(GetBaseAddress());
WS.InsertTaskCompleted += eh;
WS.InsertTaskAsync(objTask);
}
#endregion
#region GetBaseAddress
private static Uri GetBaseAddress()
{
string strBaseWebAddress = App.Current.Host.Source.AbsoluteUri;
int PositionOfClientBin =
App.Current.Host.Source.AbsoluteUri.ToLower().IndexOf(@"/clientbin");
strBaseWebAddress = Strings.Left(strBaseWebAddress, PositionOfClientBin);
Uri UriWebService =
new Uri(String.Format(@"{0}/WebService.asmx", strBaseWebAddress));
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 the TabControlModel
'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 the TabItem
.
- 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()
{
}
#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
#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()
{
colTabItems.Clear();
TasksModel.GetTasks((Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
foreach (var objTask in EventArgs.Result)
{
TabItem objTabItem = CreateTaskItem(objTask);
colTabItems.Add(objTabItem);
}
if (colTabItems.Count == 0)
{
Message = "No Records Found";
}
else
{
Message = "";
}
if (CurrentTaskID != -1)
{
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)
{
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);
if (!DesignerProperties.IsInDesignTool)
{
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)
{
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)
{
Task objTask = GetTaskFromTaskDetails((param as TaskDetails));
if (objTask.TaskID != -1)
{
DeleteTask(objTask);
}
else
{
RemoveTask(objTask.TaskID);
}
}
private bool CanDeleteTask(object param)
{
return ((param as TaskDetails) != null);
}
#endregion
#region UpdateTaskCommand
public ICommand UpdateTaskCommand { get; set; }
public void UpdateTask(object param)
{
Task objTask = GetTaskFromTaskDetails((param as TaskDetails));
if (objTask.TaskID == -1)
{
InsertTask(objTask);
}
else
{
UpdateTask(objTask);
}
}
private bool CanUpdateTask(object param)
{
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)
{
TasksModel.DeleteTask(objTask.TaskID, (Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
Message = EventArgs.Result;
RemoveTask(objTask.TaskID);
}
});
}
#endregion
#region UpdateTask
private void UpdateTask(Task objTask)
{
TasksModel.UpdateTask(objTask, (Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
Message = EventArgs.Result;
}
});
}
#endregion
#region InsertTask
private void InsertTask(Task objTask)
{
TasksModel.InsertTask(objTask, (Param, EventArgs) =>
{
if (EventArgs.Error == null)
{
CurrentTaskID = EventArgs.Result.TaskID;
GetTasks();
Message = "";
}
});
}
#endregion
You will notice that the AddNewTask
method calls the SetToNewTask()
method:
#region SetToNewTask
private void SetToNewTask()
{
foreach (var item in colTabItems)
{
item.IsSelected = false;
}
Task objTask = new Task();
objTask.TaskID = -1;
TabItem objNewTabItem = CreateTaskItem(objTask);
objNewTabItem.IsSelected = true;
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)
{
TaskDetails objTaskDetails = new TaskDetails();
TaskDetailsModel objTaskDetailsModel =
(TaskDetailsModel)objTaskDetails.DataContext;
objTaskDetailsModel.SetCurrentTask(Task);
TabItem objTabItem = new TabItem();
objTabItem.Name = string.Format("DynamicTab_{0}", Task.TaskID.ToString());
objTabItem.Style = (Style)App.Current.Resources["TabItemStyle1"];
string strTaskID = (Task.TaskID == -1) ? "[New]" : Task.TaskID.ToString();
objTabItem.Header = String.Format("Task {0}", strTaskID);
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 current Task
for it to the selected Task
(using the SetCurrentTask(Task)
method).
- It then places this View on a dynamically created
TabItem
(it sets the View as the Content
of the TabItem
).
- It then returns the
TabItem
so that the SetToNewTask()
method can add it to the colTabItems
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 a Task
and binds it to a dynamically created instance of the TaskDetailsModel
- 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.