OpenLightGroup Blog

rss

Blogs from OpenLightGroup.net


Add Tabbed Pages To Your Silverlight Project, Quick and Easy

Michael Washington made such a good case for tabbed pages that I decided to adapt a project I was already blogging about. I’d reached the point where I’d created  a simple example and now wanted to build on it. Usually, rather than go to the trouble of creating some paging mechanism, I’d just keep on coding and the source underlying the example would be gone forever. With tabbed pages it’s easy to keep each example within its own tabbed page. I’m glad I made the decision, because his example made it easy to make the change:

image

If you want to really understand what’s going on, I refer you to Michael’s article. I’m just going to supply you with code that will let you easily create and select tabbed pages.

Download Code:

http://richardwaddell.adefwebserver.com/ooNaTabbed.zip

Other Acknowledgements:

John Papa for Delegate Command

TabbedPageContainer

In the main page of my project, the Canvas that will contain the tabbed pages is named TabbedPageContainer. It doesn’t really need a name, I just gave it one so you can easily keep track of it. You can see that it contains a TabControl and that the TabControl has an AddTabItem behavior attached:

image

The StackPanel above TabbedPageContainer contains a ComboBox that will be used to select pages to load into new TabItems. When a selection is made the AddTabItem behavior is triggered to add the appropriate UserControl. First let’s look at the Behavior:

AddTabItem Behavior

}namespace ooNaTabbed
{
    public class AddTabItem : TargetedTriggerAction<TabControl>
    {
        // The AddTabItem Behavior
        // - Is intended to be triggered by a change in the SelectedIndex property as bound to by designer
        // - Determines which UseControl is to be added as the content of the new TabItem, 
        // - Creates an instance of the UserControl
        // - Creates a GenericTabHeader control with the appropriate page name to be used as the TabItem Header
        // - Adds the new UserControl and GenericTabHeader to the TabItem
        // - Sets references to TabControl and TabItem in the GenericTabHeaderViewModel
        // - Adds the TabItem to the TabControl
        TabControl _tabControl;
        #region SelectedIndexProperty
        public static readonly DependencyProperty SelectedIndexProperty = DependencyProperty.Register("SelectedIndex",
            typeof(int), typeof(AddTabItem), null);
        // Designer binds SelectedIndex to combobox SelectedIndex Property - TwoWay binding
        public int SelectedIndex
        {
            get {return (int)base.GetValue(SelectedIndexProperty);}
            set {base.SetValue(SelectedIndexProperty, value);}
        }
        #endregion
        protected override void OnAttached()
        {
            base.OnAttached();
            _tabControl = AssociatedObject as TabControl;
        }
        // The behavior is triggered
        protected override void Invoke(object parameter)
        {
            AddTabToTabControl();
        }
        protected override void OnDetaching()
        {
            base.OnDetaching();
        }
        private void AddTabToTabControl()
        {
            // Bail if appropriate
            if (_tabControl == null || SelectedIndex < 0)
                return;
            // The header is just the name and a button to close the tab
            GenericTabHeader tabHeader = new GenericTabHeader();
            // We get the ViewModel behind the GenericTabHeader UserControl, bound to by the designer
            GenericTabHeaderViewModel headerVM = tabHeader.DataContext as GenericTabHeaderViewModel;
            if (headerVM != null)
            {
                // Create the TabItem that will be added to the TabControl
                TabItem tabItem = new TabItem();
                // Determine which UserControl to create and what to display in the header
                switch (SelectedIndex)
                {
                    case 0:
                        tabItem.Content = new XamlPathList();
                        headerVM.HeaderDisplay = "Design-Time PathListBox";
                        break;
                    case 1:
                        tabItem.Content = new DynamicPathList();
                        headerVM.HeaderDisplay = "Run-Time PathListBox";
                        break;
                    default:
                        headerVM.HeaderDisplay = string.Format("Unknown selection: {0}", SelectedIndex);
                        break;
                }
                // Use the header we just created
                tabItem.Header = tabHeader;
                // The GenericTabHeaderViewModel needs references to the TabControl and TabItem so it can close the tab when the user clicks on the close button
                headerVM.TabControlClient = _tabControl;
                headerVM.TabItemClient = tabItem;
                // And finally, add the new TabItem to the TabControl
                _tabControl.Items.Add(tabItem);
            }
        }
    }
}

GenericTabHeaderViewModel

GenericTabHeader is a UserControl used to supply the contents of the TabItem.Header. GenericTabHeaderViewModel provides the functionality:

namespace ooNaTabbed
{
    public class GenericTabHeaderViewModel : INotifyPropertyChanged
    {
        // GenericTabHeaderViewModel has two functions
        // - Maintain a HeaderDisplay string property for the tab header name display to bind to
        // - Support a CloseTabCommand ICommand implementation for the tab header close button to bind to
        #region Constructor(s)
        public GenericTabHeaderViewModel()
        {
            CloseTabCommand = new DelegateCommand(CloseTab, CanCloseTab);
        }
        #endregion
        #region CloseTabCommand
        public ICommand CloseTabCommand { get; set; }
        public void CloseTab(object param)
        {
            // Remove this TabItem from the Tab control
            TabControlClient.Items.Remove(TabItemClient);
        }
        private bool CanCloseTab(object param)
        {
            return TabControlClient != null && TabItemClient != null;
        }
        #endregion
        // TabControlClient and TabItemClient are set by the AddTabItem behavior
        #region TabControlClient
        private TabControl _TabControlClient;
        public TabControl TabControlClient
        {
            get { return this._TabControlClient; }
            set
            {
                if (this._TabControlClient != value)
                {
                    this._TabControlClient = value;
                    this.NotifyPropertyChanged("TabControlClient");
                }
            }
        }
        #endregion
        #region TabItemClient
        private TabItem _TabItemClient;
        public TabItem TabItemClient
        {
            get { return this._TabItemClient; }
            set
            {
                if (this._TabItemClient != value)
                {
                    this._TabItemClient = value;
                    this.NotifyPropertyChanged("TabItemClient");
                }
            }
        }
        #endregion
        #region HeaderDisplay
        private string _HeaderDisplay = "Tab Header";
        public string HeaderDisplay
        {
            get { return this._HeaderDisplay; }
            set
            {
                if (this._HeaderDisplay != value)
                {
                    this._HeaderDisplay = value;
                    this.NotifyPropertyChanged("HeaderDisplay");
                }
            }
        }
        #endregion

Bringing it All Together

Here’s the binding to the behavior:

image

You can’t see it clearly, but the SourceObject is set to the ComboBox, and as you can see the behavior is triggered onthe SelectionChangedEvent.

And here’s the binding of the Behavior SelectedIndex property to the SelectedIndex property of the ComboBox:

image 

The upshot being that the change in the ComboBox SelectionChanged  event triggers the behavior, which then uses the property that changes as a result of the event, SelectedIndex, to determine which UserControl to create as content for the new TabItem.

Speaking of which, here’s how the ComboBox gets loaded:

The Grungy Part

image

Summary

As I mentioned in the beginning, this is a quick way to add tabbed pages to your project, and what’s quicker than adding hard-coded list items and determining which is which by their offset in the list? In anything other than a demonstration application you’ll probably want this part of the code to be more robust. In that case you can bind to the ComboBox SelectedItem or SelectedValue properties. When you load the ComboBox you would set the Value property of each ComboBoxListItem to some value, such as an enum, that would specifically identify the selection no matter what order the list is in.

Fixes

I guess I should point out that the content of the pages has nothing to do with the subject of the article. They’re part of a project I was working on when I decided to switch to tabbed pages. But even so, the problem that Michael Washington found (and fixed) on the first page should not stand. The animation needs to stop when the user switches away from the page:

image

I also decided I wanted the first item in the ComboBox selected automatically, so I added a SelectFirstItem Behavior to be triggered by the ComboBox Loaded event (bound by the Designer in Blend – not shown):

namespace ooNaTabbed
{
    public class SelectFirstItem : TargetedTriggerAction<TabControl>
    {
        private ComboBox _ComboBox;
        protected override void OnAttached()
        {
            base.OnAttached();
            _ComboBox = AssociatedObject as ComboBox;
        }
        protected override void Invoke(object parameter)
        {
            if (_ComboBox != null && _ComboBox.Items.Count > 0)
                _ComboBox.SelectedIndex = 0;
        }
        protected override void OnDetaching()
        {
            base.OnDetaching();
        }
    }
}

The problem is that AddTabItem behavior is triggered on the ComboBox SelectionChanged event. which is not triggered by programmatically changing the value of CombobBox SelectedIndex. So the AddTabItem behavior must be triggered on the ComboBox SelectedIndex PropertyChanged event:

image

image

The change is subtle, but important. Instead of being triggered by the ComboBox SelectionChanged event, the AddTabItem Behavior is triggered by the change in the ComboBox SelectedIndex property. AddTabItems is now bound to SelectedIndex in two ways. It’s triggered when the property changes, and the behavior SelectedIndex property is bound to the ComboBox SelectedIndex property – which actually makes the most sense – a change in the property you’re interested in causes you to do something with that property, or more accurately the local copy of the property which is kept current through binding. Keep in mind, however, that type of binding, where a change to the UI sets the underlying property is only supported if you set Binding Direction to TwoWay as you see below:

image

And one last note; I found that as I add pages I prefer the default tab to open at startup be the last one in the list, so I created an OpenLastItem behavior with the following change to the Invoke method:

        protected override void Invoke(object parameter)
        {
            if (_ComboBox != null && _ComboBox.Items.Count > 0)
                _ComboBox.SelectedIndex = _ComboBox.Items.Count - 1;
        }




Comments are closed.
Showing 1 Comment
Avatar  Michael Washington 7 years ago

Looks great!