A User Interface That You Will Either Love Or Hate
Live example: http://silverlight.adefwebserver.com/SimpleAttendanceTabWeb
I love Tabs. Using Tabs is like placing sheets of paper on your desk for easy retrieval, rather than going into your file cabinet each time you need something.
I am working on a Silverlight client for my popular ADefHelpDesk.com Open Source project, and after watching users of the current program open up multiple web browser windows, to easily return to a Help Desk Ticket they were working on, I realized that the Silverlight version needed to use Tabs.
The problem I ran into was, that there was no easy way to have the main search screen as a Tab, and the other Help Desk Ticket Tabs in the same Tab control. A Silverlight Tab control wants to bind to a collection of a single type.
The answer, as in most cases, is to simply use Behaviors.
The Application
The previous version of the application, covered in the article, Silverlight Attendance Demo using Sterling Silverlight Database, allowed you to select only one day at a time.
In this version, we add the following features:
- The first Tab is a tabulation of the Attendance totals for each Student, this Tab cannot be closed.
- When you select the Overview Tab, its calculations are always automatically updated.
- Unlimited Tabs can be opened for each day that allow you to enter Attendance.
- If you select a Tab, using the Calendar, that is already opened, it will simply switch to that Tab.
- Sorting works for all columns except for columns that have radio buttons.
To select a day to enter Attendance, you can click on the Icon next to the Attendance box, to bring up the Calendar, and select a day. After selecting a day, you click the Open day button, to open that day in a Tab.
Tab Header Controls
The first thing we do is make Silverlight controls that will represent the Tab headers for the Overview Tab and the Attendance Tabs.
The Overview Tab header (MainTabHeader.xaml) is simply a control with the words "Overview" in a TextBlock. The only reason we go through the trouble to make a control, is so that it can be easily styled by a Designer.
The Attendance Tab header (AttendanceTabHeader.xaml) is slightly more complex as it has a View Model. The View Model allows the text to be dynamically set and it contains a ICommand
to allow the Tab to be closed.
It also contains an instance of the Tab Control, and the "Tab the header is on", so that the Tab can be closed.
This is the ICommand
that closes the Tab:
public ICommand CloseTabCommand { get; set; }
public void CloseTab(object param)
{
objTabControl.Items.Remove(objTabItem);
}
private bool CanCloseTab(object param)
{
return true;
}
The Tab Content Controls
The Overview.xaml control contains the Attendance
totals for each Student
.
It contains an ICommand
that is triggered by the LayoutRoot Loading. This causes it to be refreshed, whenever the Tab that it is on, gets focus.
public ICommand ComputeTotalsCommand { get; set; }
public void ComputeTotals(object param)
{
if (!(DesignerProperties.IsInDesignTool))
{
LoadData();
}
}
private bool CanComputeTotals(object param)
{
return true;
}
The following method computes the totals:
public void LoadData()
{
bool hasKeys = false;
foreach (var item in SterlingService.Current.Database.Query<Student, int>())
{
hasKeys = true;
break;
}
if (hasKeys)
{
Students.Clear();
Enrollments.Clear();
colAttendance.Clear();
colStudentOverview.Clear();
foreach (var item in SterlingService.Current.Database.Query<Student, int>())
{
Students.Add(item.LazyValue.Value);
}
foreach (var item in SterlingService.Current.Database.Query<Enrollment, int>())
{
Enrollments.Add(item.LazyValue.Value);
}
foreach (var item in SterlingService.Current.Database.Query<Attendance, string>())
{
colAttendance.Add(item.LazyValue.Value);
}
var result = from Student in Students
select new StudentOverview
{
StudentId = Student.StudentId,
Name = Student.Name,
P = (from objEnrollment in Enrollments
from objAttendance in colAttendance
where objEnrollment.StudentId == Student.StudentId
where objEnrollment.EnrollmentId ==
objAttendance.EnrollmentId
where objAttendance.AttendanceStatus == "P"
select objAttendance).Count(),
T = (from objEnrollment in Enrollments
from objAttendance in colAttendance
where objEnrollment.StudentId == Student.StudentId
where objEnrollment.EnrollmentId ==
objAttendance.EnrollmentId
where objAttendance.AttendanceStatus == "T"
select objAttendance).Count(),
E = (from objEnrollment in Enrollments
from objAttendance in colAttendance
where objEnrollment.StudentId == Student.StudentId
where objEnrollment.EnrollmentId ==
objAttendance.EnrollmentId
where objAttendance.AttendanceStatus == "E"
select objAttendance).Count(),
U = (from objEnrollment in Enrollments
from objAttendance in colAttendance
where objEnrollment.StudentId == Student.StudentId
where objEnrollment.EnrollmentId ==
objAttendance.EnrollmentId
where objAttendance.AttendanceStatus == "U"
select objAttendance).Count(),
};
foreach (var Student in result)
{
colStudentOverview.Add(Student);
}
}
}
The AttendanceDay.xaml control contains the Attendance
for Students
enrolled on a single day. Most of the code for this control is covered in the article, Silverlight Attendance Demo using Sterling Silverlight Database.
The Behaviors
The thing that ties all this together is two simple Behaviors.
AddOverviewToTabControl Behavior
The AddOverviewToTabControl
Behavior is the simpler of the two. It is attached to the Tab Control, and all it does is dynamically create a TabItem
and place the Overview.xaml control on the Tab Control.
First, we place a Tab Control on the MainPage.xaml control.
Next, we add a AddOverviewToTabControl
Behavior to the TabControl
.
We then bind the Behavior
to the Grid
that is the Layoutroot
. We use an EventTrigger
that will fire when the Grid
is Loaded.
Here is the code for the Behavior
:
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace SimpleAttendance
{
[System.ComponentModel.Description("Adds the OverView control to a Tab Control")]
public class AddOverviewToTabControl : TargetedTriggerAction<TabControl>
{
TabControl objTabControl;
protected override void OnAttached()
{
base.OnAttached();
objTabControl = (TabControl)(this.AssociatedObject);
}
protected override void OnDetaching()
{
base.OnDetaching();
}
protected override void Invoke(object parameter)
{
AddTabToTabControl();
}
private void AddTabToTabControl()
{
TabItem objTabItem = new TabItem();
MainTabHeader objMainTabHeader = new MainTabHeader();
Overview objOverview = new Overview();
objTabItem.Header = objMainTabHeader;
objTabItem.Content = objOverview;
objTabControl.Items.Add(objTabItem);
}
}
}
AddAttendanceToTabControl Behavior
The AddAttendanceToTabControl
Behavior is slightly more complicated, because it needs to know what Attendance
day to create a Tab for.
The Behavior
is raised by a change in the AttendanceDate
Property (when the Open day button raises an ICommand
). The value of the AttendanceDate
Property is bound to a Dependency Property in the Behavior. Here is the code for the Behavior:
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace SimpleAttendance
{
[System.ComponentModel.Description("Adds te Attendance control to a Tab Control")]
public class AddAttendanceToTabControl : TargetedTriggerAction<TabControl>
{
TabControl objTabControl;
#region AttendanceDayProperty
public static readonly DependencyProperty AttendanceDayProperty =
DependencyProperty.Register("AttendanceDay",
typeof(DateTime?), typeof(AddAttendanceToTabControl), null);
public DateTime? AttendanceDay
{
get
{
return (DateTime?)base.GetValue(AttendanceDayProperty);
}
set
{
base.SetValue(AttendanceDayProperty, value);
}
}
#endregion
protected override void OnAttached()
{
base.OnAttached();
objTabControl = (TabControl)(this.AssociatedObject);
}
protected override void OnDetaching()
{
base.OnDetaching();
}
protected override void Invoke(object parameter)
{
if (AttendanceDay != null)
{
var Tab = (from Tabs in objTabControl.Items.Cast<TabItem>()
where (Tabs.Tag as string) ==
AttendanceDay.Value.Ticks.ToString()
select Tabs).FirstOrDefault();
if (Tab == null)
{
AddTabToTabControl();
}
else {
Tab.IsSelected = true;
}
}
}
private void AddTabToTabControl()
{
TabItem objTabItem = new TabItem();
AttendanceDay objAttendanceDay = new AttendanceDay();
AttendanceDayModel objAttendanceDayModel =
(AttendanceDayModel)objAttendanceDay.DataContext;
objAttendanceDayModel.SetDateCommand.Execute(AttendanceDay);
AttendanceTabHeader objAttendanceTabHeader = new AttendanceTabHeader();
AttendanceTabHeaderModel objAttendanceTabHeaderModel =
(AttendanceTabHeaderModel)objAttendanceTabHeader.DataContext;
objAttendanceTabHeaderModel.HeaderDisplay =
String.Format("{0} {1}", AttendanceDay.Value.DayOfWeek.ToString(),
AttendanceDay.Value.ToShortDateString());
objAttendanceTabHeaderModel.objTabControl = objTabControl;
objAttendanceTabHeaderModel.objTabItem = objTabItem;
objTabItem.Tag = AttendanceDay.Value.Ticks.ToString();
objTabItem.Header = objAttendanceTabHeader;
objTabItem.Content = objAttendanceDay;
objTabControl.Items.Add(objTabItem);
objTabItem.IsSelected = true;
}
}
}
Behaviors Are The Key
I originally created another solution to the challenge that I was facing. It consisted of a custom Tab Control and an extensive use of templates. However, when I looked at what I had so far, I knew I would avoid making any changes or enhancements, because it was already so complicated. We find ourselves sometimes creating these situations when using View Model / MVVM.
Using Behaviors allows us to avoid overly complex architecture. Behaviors are mostly small atomic operations with minimal input and output. They are easily reused, and easily consumed by non-programmers such as Designers. Mostly, they operate from the "UI side of things". If we think of the View Model as being behind the View, we can think of Behaviors as being in front of the View. Addressing architectural challenges from two different sides opens up a lot of possible solutions.
History
- 14th November, 2010: Initial post