Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Silverlight Super Tabs Interface (using View Model / MVVM)

0.00/5 (No votes)
14 Nov 2010 2  
Dynamically creating tabs of different types in the same Tab Control

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)
{
    // Remove this TabItem from the Tab control
    objTabControl.Items.Remove(objTabItem);
}

private bool CanCloseTab(object param)
{
    return true;
}

The Tab Content Controls

Click to enlarge image

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)
    {
        // Clear All Collections
        Students.Clear();
        Enrollments.Clear();
        colAttendance.Clear();
        colStudentOverview.Clear();
        
        // Get the data
        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);
        }
        
        // Create the Query
        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(),
                        };
                        
        // Fill the final Collection
        foreach (var Student in result)
        {
            colStudentOverview.Add(Student);
        }
    }
}

Click to enlarge image

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.

Click to enlarge image

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()
        {
            // Make a New TabItem
            TabItem objTabItem = new TabItem();

            // Make a instance of the MainTabHeader.xaml control
            MainTabHeader objMainTabHeader = new MainTabHeader();

            // Make a instance of the Overview.xaml control
            Overview objOverview = new Overview();

            // Set the Header to the MainTabHeader.xaml Control 
            objTabItem.Header = objMainTabHeader;
            // Set the Content to the Overview.xaml Control
            objTabItem.Content = objOverview;

            // Add the TabItem to the TabControl
            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. 

Click to enlarge image

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)
            {
                // See if the Tab is already added
                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 already exists
                {
                    // Set the Tab as selected
                    Tab.IsSelected = true;
                }
            }
        }

        private void AddTabToTabControl()
        {
            TabItem objTabItem = new TabItem();

            // ** Tab Content **
            AttendanceDay objAttendanceDay = new AttendanceDay();
            // Get it's DataContext
            AttendanceDayModel objAttendanceDayModel =
                (AttendanceDayModel)objAttendanceDay.DataContext;
            // Set the date
            objAttendanceDayModel.SetDateCommand.Execute(AttendanceDay);

            // ** Tab Header **
            AttendanceTabHeader objAttendanceTabHeader = new AttendanceTabHeader();
            // Get it's DataContext
            AttendanceTabHeaderModel objAttendanceTabHeaderModel =
                (AttendanceTabHeaderModel)objAttendanceTabHeader.DataContext;

            // Set the Header Display
            objAttendanceTabHeaderModel.HeaderDisplay =
                String.Format("{0} {1}", AttendanceDay.Value.DayOfWeek.ToString(),
                AttendanceDay.Value.ToShortDateString());

            // Pass an instance of the TabControl to the View Model
            // to allow this Tab to be removed from it
            objAttendanceTabHeaderModel.objTabControl = objTabControl;
            // Pass an instance of this TabItem to the View Model
            // to allow this Tab to be removed from the TabControl
            objAttendanceTabHeaderModel.objTabItem = objTabItem;

            // Set the Tag on this Tab to the Ticks so we can easily find it 
            // in the Invoke method of this Behavior
            objTabItem.Tag = AttendanceDay.Value.Ticks.ToString();

            objTabItem.Header = objAttendanceTabHeader;
            objTabItem.Content = objAttendanceDay;
            objTabControl.Items.Add(objTabItem);

            // Set the Tab as selected
            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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here