Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C# 4.0

Silverlight Super Tabs Interface (using View Model / MVVM)

Rate me:
Please Sign up or sign in to vote.
4.94/5 (17 votes)
14 Nov 2010Ms-PL4 min read 88.7K   2.9K   47   28
Dynamically creating tabs of different types in the same Tab Control
Image 1

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

Image 2

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.

Image 3

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

Image 4

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.

Image 5

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:

C#
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.

C#
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:

C#
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

Image 8

The thing that ties all this together is two simple Behaviors.

AddOverviewToTabControl Behavior

Image 9

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.

Image 10

First, we place a Tab Control on the MainPage.xaml control.

Image 11

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:

C#
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

Image 13

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:

C#
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, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer (Senior) http://ADefWebserver.com
United States United States
Michael Washington is a Microsoft MVP. He is a ASP.NET and
C# programmer.
He is the founder of
AiHelpWebsite.com,
LightSwitchHelpWebsite.com, and
HoloLensHelpWebsite.com.

He has a son, Zachary and resides in Los Angeles with his wife Valerie.

He is the Author of:

Comments and Discussions

 
QuestionGetting error: Unable to cast object Pin
Member 108005568-May-14 3:32
Member 108005568-May-14 3:32 
AnswerRe: Getting error: Unable to cast object Pin
defwebserver8-May-14 5:58
defwebserver8-May-14 5:58 
QuestionHow can i use tab item inside tab control and then set its content? Pin
Member 108005567-May-14 5:21
Member 108005567-May-14 5:21 
AnswerRe: How can i use tab item inside tab control and then set its content? Pin
defwebserver7-May-14 5:42
defwebserver7-May-14 5:42 
QuestionVery nice post! Pin
Member 859903928-Jan-12 11:15
Member 859903928-Jan-12 11:15 
AnswerRe: Very nice post! Pin
defwebserver28-Jan-12 11:36
defwebserver28-Jan-12 11:36 
GeneralRe: Very nice post! Pin
Member 859903928-Jan-12 11:40
Member 859903928-Jan-12 11:40 
GeneralRe: Very nice post! Pin
defwebserver28-Jan-12 11:46
defwebserver28-Jan-12 11:46 
GeneralRe: Very nice post! Pin
Member 859903928-Jan-12 11:48
Member 859903928-Jan-12 11:48 
QuestionCPU usage Pin
aiads19-Sep-11 22:38
aiads19-Sep-11 22:38 
AnswerRe: CPU usage Pin
defwebserver20-Sep-11 2:51
defwebserver20-Sep-11 2:51 
GeneralRe: CPU usage Pin
aiads20-Sep-11 22:21
aiads20-Sep-11 22:21 
GeneralMy vote of 5 Pin
Serdar YILMAZ30-May-11 21:39
Serdar YILMAZ30-May-11 21:39 
GeneralRe: My vote of 5 Pin
defwebserver20-Sep-11 2:50
defwebserver20-Sep-11 2:50 
QuestionDelegate Command really works? Pin
Antxon25-Mar-11 0:26
Antxon25-Mar-11 0:26 
AnswerRe: Delegate Command really works? Pin
defwebserver25-Mar-11 2:17
defwebserver25-Mar-11 2:17 
GeneralMy vote of 5 Pin
Ritesh Ramesh21-Nov-10 15:37
Ritesh Ramesh21-Nov-10 15:37 
GeneralRe: My vote of 5 Pin
defwebserver22-Nov-10 2:06
defwebserver22-Nov-10 2:06 
GeneralMy vote of 5 Pin
Richard Waddell17-Nov-10 16:02
Richard Waddell17-Nov-10 16:02 
Really good article. I expected this to be really complex, but everything ties together so logically (dare I say elegantly?) that it's easy to understand.
GeneralRe: My vote of 5 Pin
defwebserver18-Nov-10 4:19
defwebserver18-Nov-10 4:19 
GeneralMy vote of 5 Pin
linuxjr16-Nov-10 7:21
professionallinuxjr16-Nov-10 7:21 
GeneralRe: My vote of 5 Pin
defwebserver16-Nov-10 14:16
defwebserver16-Nov-10 14:16 
GeneralMy vote of 5 Pin
MyTatuo15-Nov-10 5:18
MyTatuo15-Nov-10 5:18 
GeneralRe: My vote of 5 Pin
defwebserver15-Nov-10 5:21
defwebserver15-Nov-10 5:21 
GeneralMy vote of 5 Pin
indyfromoz14-Nov-10 16:57
indyfromoz14-Nov-10 16:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.