Walkthrough: Creating a Shell Extension

This walkthrough demonstrates how to create a shell extension for LightSwitch. The shell for a LightSwitch application enables users to interact with the application. It manifests the navigable items, running screens, associated commands, current user information, and other useful information that are part of the shell’s domain. While LightSwitch provides a straightforward and powerful shell, you can create your own shell that provides your own creative means for interacting with the various parts of the LightSwitch application.

In this walkthrough, you create a shell that resembles the default shell, but with several subtle differences in appearance and behavior. The command bar eliminates the command groups and moves the Design Screen button to the left. The navigation menu is fixed in position, no Startup screen is displayed, and screens are opened by double-clicking menu items. The screens implement a different validation indicator, and current user information is always displayed in the lower-left corner of the shell. These differences will help illustrate several useful techniques for creating shell extensions.

The anatomy of a shell extension consists of three main parts:

  • The Managed Extensibility Framework (MEF), which exports the implementation of the shell contract.

  • The Extensible Application Markup Language (XAML), which describes the controls that are used for the shell UI.

  • The Visual Basic or C# code behind the XAML, which implements the behavior of the controls and interacts with the LightSwitch run time.

Creating a shell extension involves the following tasks:

  • Visual Studio 2013 Professional

  • Visual Studio 2013 SDK

  • LightSwitch Extensibility Toolkit for Visual Studio 2013

The first step is to create a project and add a LightSwitch Shell template.

To create an extension project

  1. On the menu bar in Visual Studio, choose File, New, Project.

  2. In the New Project dialog box, expand the Visual Basic or Visual C# node, expand the LightSwitch node, choose the Extensibility node, and then choose the LightSwitch Extension Library template.

  3. In the Name field, enter ShellExtension as the name for your extension library.

  4. Choose the OK button to create a solution that contains the seven projects that are required for the extension.

To choose an extension type

  1. In Solution Explorer, choose the ShellExtension.Lspkg project.

  2. On the menu bar, choose Project, Add New Item.

  3. In the Add New Item dialog box, choose Shell.

  4. In the Name field, enter ShellSample as the name for your extension.

  5. Choose the OK button. Files will be added to several projects in your solution.

The shell extension will have to reference some namespaces that are not part of the default template.

To add references

  1. In Solution Explorer, open the shortcut menu for the ShellExtension.Client project, and choose Add Reference.

  2. In the Add Reference dialog box, add a reference to System.Windows.Controls.dll.

  3. In the Add Reference dialog box, add a reference to Microsoft.LightSwitch.ExportProvider.dll.

    You can find the assembly in the PrivateAssembly folder under the Visual Studio IDE folder.

To make an implementation of a shell available to MEF, you have to create a class that implements the IShell interface and provide the necessary attribute decorations. There are two such attributes that are required: Export and Shell. The Export attribute tells MEF which contract your class implements, whereas the Shell attribute contains the metadata that is used to differentiate your implementation of a shell from other implementations. The implementation is added by the project template and can be found in the Presentation, Shells, Components folder of the ShellExtension.Client project. The following example shows the implementation.

[Export(typeof(IShell))]
    [Shell(ShellSample.ShellId)]
    internal class ShellSample : IShell
    {
     ...
    }

The data specified in the Shell attribute is the identifier for your shell. The value must be of the following form: <Module Name>:<Shell Name>. The name of the module is specified in the module.lsml file that describes the module. This file and the module name are generated by the project template. The name of the shell is specified in the ProjectName.lsml file that describes the shell. The IShell interface has two methods: one that returns the name of the shell, which is equivalent to the value specified in the Shell attribute, and one that returns a Uri to the XAML that is an embedded resource in the built assembly.

When developing a shell extension for LightSwitch, you have the freedom to create and use any controls that provide the experience that you want. The content of every LightSwitch application consists of a set of known parts. The following table shows the defined parts of a LightSwitch application.

Part

View model

Description

Navigation

NavigationViewModel

Provides access to the Navigation pane used for opening screens.

Commands

CommandsViewModel

Provides access to the Command Bar pane used for displaying buttons or other commands.

Active Screens

ActiveScreensViewModel

Provides access to the Screen pane used for displaying screens.

Current User

CurrentUserViewModel

Enables displaying information about the current logged on user.

Logo

LogoViewModel

Enables the display of an image specified in the Logo property.

Screen Validation

ValidationViewModel

Provides access to the validation UI.

For each of these parts, LightSwitch provides a view model to which your controls can bind. LightSwitch provides a mechanism that makes the binding to these view models easy: the ComponentViewModelService. When specified as an attribute for a control, this service uses MEF to find the specified view model, instantiate it, and set it to be the data context for the control. The following code example shows how a list box has its data context set to be the commands view model.

<ListBox x:Name="CommandPanel" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource RibbonBackgroundBrush}"
                 ShellHelpers:ComponentViewModelService.ViewModelName="Default.CommandsViewModel"
                 ItemsSource="{Binding ShellCommands}">
        ...
        </ListBox>

To define the shell

  1. In Solution Explorer, in the ShellExtension.Client project, choose the Presentation, Shells folder, and then open the ShellSample.xaml file.

  2. Replace the contents with the following.

    <UserControl x:Class="ShellExtension.Presentation.Shells.ShellSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:windows="clr-namespace:System.Windows;assembly=System.Windows.Controls"
        xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
        xmlns:ShellHelpers="clr-namespace:Microsoft.LightSwitch.Runtime.Shell.Helpers;assembly=Microsoft.LightSwitch.Client"
        xmlns:local="clr-namespace:ShellExtension.Presentation.Shells">
    
        <UserControl.Resources>
            <ResourceDictionary>
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="/ShellExtension.Client;component/Presentation/Shells/TextBlockStyle.xaml" />
                </ResourceDictionary.MergedDictionaries>
            </ResourceDictionary>
    
            <!-- Convert the boolean value indicating whether or not the workspace is dirty to a Visibility value. -->
            <local:WorkspaceDirtyConverter x:Key="WorkspaceDirtyConverter" />
    
            <!-- Convert the boolean value indicating whether or not the screen has errors to a Visibility value. -->
            <local:ScreenHasErrorsConverter x:Key="ScreenHasErrorsConverter" />
    
            <!-- Convert the enumeration of errors into a single string. -->
            <local:ScreenResultsConverter x:Key="ScreenResultsConverter" />
    
            <!-- Convert the current user to a "default" value when authentication is not enabled. -->
            <local:CurrentUserConverter x:Key="CurrentUserConverter" />
    
            <!-- Template that is used for the header of each tab item: -->
            <DataTemplate x:Key="TabItemHeaderTemplate">
                <Border BorderBrush="{StaticResource ScreenTabBorderBrush}">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Style="{StaticResource TextBlockFontsStyle}" Text="{Binding DisplayName}" Foreground="{StaticResource ScreenTabTextBrush}" />
                        <TextBlock Style="{StaticResource TextBlockFontsStyle}" Text="*" 
                                   Visibility="{Binding IsDirty, Converter={StaticResource WorkspaceDirtyConverter}}" 
                                   Margin="5, 0, 5, 0" />
                        <TextBlock Style="{StaticResource TextBlockFontsStyle}" Text="!" 
                                   Visibility="{Binding ValidationResults.HasErrors, Converter={StaticResource ScreenHasErrorsConverter}}" 
                                   Margin="5, 0, 5, 0" Foreground="Red" FontWeight="Bold">
                            <ToolTipService.ToolTip>
                                <ToolTip Content="{Binding ValidationResults, Converter={StaticResource ScreenResultsConverter}}" />
                            </ToolTipService.ToolTip>
                        </TextBlock>
                        <Button Height="16"
                                Width="16"
                                Padding="0"
                                Margin="5, 0, 0, 0"
                                Click="OnClickTabItemClose">X</Button>
                    </StackPanel>
                </Border>
            </DataTemplate>
        </UserControl.Resources>
    
        <Grid x:Name="LayoutRoot" Background="{StaticResource NavShellBackgroundBrush}">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="5*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
    
            <!-- The command panel is a horizontally oriented list box whose data context is set to the  -->
            <!-- CommandsViewModel.  The ItemsSource of this list box is data bound to the ShellCommands -->
            <!-- property.  This results in each item being bound to an instance of an IShellCommand.    -->
            <!--                                                                                         -->
            <!-- The attribute 'ShellHelpers:ComponentViewModelService.ViewModelName' is the manner by   -->
            <!-- which a control specifies the view model that is to be set as its data context.  In     -->
            <!-- case, the view model is identified by the name 'Default.CommandsViewModel'.             -->
            <ListBox x:Name="CommandPanel" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource RibbonBackgroundBrush}"
                            ShellHelpers:ComponentViewModelService.ViewModelName="Default.CommandsViewModel"
                            ItemsSource="{Binding ShellCommands}">
    
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal" />
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <!-- Each item in the list box will be a button whose content is the following:         -->
                        <!--    1.  An image, which is bound to the Image property of the IShellCommand         -->
                        <!--    2.  A text block whose text is bound to the DisplayName of the IShellCommand   -->
                        <StackPanel Orientation="Horizontal">
                            <!-- The button be enabled or disabled according to the IsEnabled property of  the -->
                            <!-- IShellCommand.  The handler for the click event will execute the command.  -->
                            <Button Click="GeneralCommandHandler"
                                    IsEnabled="{Binding IsEnabled}"
                                    Style="{x:Null}"
                                    Background="{StaticResource ButtonBackgroundBrush}"
                                    Margin="1">
    
                                <Grid>
                                    <Grid.RowDefinitions>
                                        <RowDefinition Height="32" />
                                        <RowDefinition MinHeight="24" Height="*"/>
                                    </Grid.RowDefinitions>
                                    <Image Grid.Row="0"
                                           Source="{Binding Image}"
                                           Width="32"
                                           Height="32"
                                           Stretch="UniformToFill"
                                           Margin="0"
                                           VerticalAlignment="Top"
                                           HorizontalAlignment="Center" />
                                    <TextBlock Grid.Row="1"
                                               Text="{Binding DisplayName}"
                                               TextAlignment="Center"
                                               TextWrapping="Wrap"
                                               Style="{StaticResource TextBlockFontsStyle}"
                                               MaxWidth="64" />
                                </Grid>
                            </Button>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
    
            <!-- Navigation view is a simple tree view whose ItemsSource property is bound to -->
            <!-- the collection returned from the NavigationItems property of the Navigation  -->
            <!-- view model.                                                                  -->
            <controls:TreeView x:Name="ScreenTree" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2"
                      Background="{StaticResource NavShellBackgroundBrush}"
                      ShellHelpers:ComponentViewModelService.ViewModelName="Default.NavigationViewModel"
                      ItemsSource="{Binding NavigationItems}"
                      Loaded="OnTreeViewLoaded">
                <controls:TreeView.ItemTemplate>
                    <!-- Each navigation item may have children, so set up the binding to the -->
                    <!-- Children property of the INavigationGroup                            -->
                    <windows:HierarchicalDataTemplate ItemsSource="{Binding Children}">
                        <!-- Each item in the TreeView is a TextBlock whose text value is bound to the DisplayName property of the INavigationItem -->
                        <TextBlock Style="{StaticResource TextBlockFontsStyle}" 
                                   Text="{Binding DisplayName}" 
                                   Foreground="{StaticResource NormalFontBrush}" 
                                   MouseLeftButtonDown="NavigationItemLeftButtonDown" />
                    </windows:HierarchicalDataTemplate>
                </controls:TreeView.ItemTemplate>
            </controls:TreeView>
    
            <controls:GridSplitter Grid.Column="0"
                              Grid.Row="1"
                              Grid.RowSpan="2"
                              Style="{x:Null}"
                              Width="5"
                              Name="gridSplitter1"
                              Background="Transparent"
                              HorizontalAlignment="Right"
                              VerticalAlignment="Stretch" />
    
            <!-- Each screen will be displayed in a tab in a tab control.  The individual TabItem -->
            <!-- controls are created in code.                                                    -->
            <controls:TabControl x:Name="ScreenArea"
                                 Grid.Column="1"
                                 Grid.Row="1"
                                 Grid.RowSpan="2"
                                 Background="{StaticResource NavShellBackgroundBrush}"
                                 SelectionChanged="OnTabItemSelectionChanged">
            </controls:TabControl>
    
            <!-- The name of the current user is displayed in the lower-left corner of the shell. -->
            <Grid Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
    
                <TextBlock Grid.Column="0" Style="{StaticResource TextBlockFontsStyle}" Text="Current User: " Foreground="{StaticResource NormalFontBrush}"/>
    
                <!-- This TextBlock has its data context set to the CurrentUserViewModel, from which the -->
                <!-- CurrentUserDisplayName property is used to provide the name of the user displayed.  -->
                <TextBlock Grid.Column="1"
                           Style="{StaticResource TextBlockFontsStyle}"
                           ShellHelpers:ComponentViewModelService.ViewModelName="Default.CurrentUserViewModel"
                           Text="{Binding CurrentUserDisplayName, Converter={StaticResource CurrentUserConverter}}"
                           Foreground="{StaticResource NormalFontBrush}"/>
            </Grid>
        </Grid>
    </UserControl>
    

    This contains the complete code listing for the ShellSample.xaml file; the different sections will be explained in later steps. You can ignore any errors regarding missing types; these will also be added later.

  3. In Solution Explorer, open the shortcut menu for the Presentation, Shells node in the ShellExtension.Client project and choose Add New Item.

  4. In the Add New Item dialog box, expand the Silverlight node, and then choose Silverlight Resource Dictionary.

  5. In the Name field, enter TextBlockStyle, and then choose the Add button.

  6. Replace the existing XAML with the following to define a ResourceDictionary.

    <ResourceDictionary
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Microsoft.LightSwitch.Presentation.Framework.Helpers;assembly=Microsoft.LightSwitch.Client">
    
        <Style x:Key="TextBlockFontsStyle" TargetType="TextBlock">
            <Setter Property="FontFamily" Value="{StaticResource NormalFontFamily}" />
            <Setter Property="FontSize" Value="{StaticResource NormalFontSize}" />
            <Setter Property="FontWeight" Value="{StaticResource NormalFontWeight}" />
            <Setter Property="FontStyle" Value="{StaticResource NormalFontStyle}" />
        </Style>
        
    </ResourceDictionary>
    

    The resource dictionary is referenced by the UserControl XAML and specifies the style that is applied to all of the TextBlock controls.

The XAML for the shell references several value converters; you will define them next.

To define value converters

  1. In Solution Explorer, in the ShellExtension.Client project, open the shortcut menu for the Presentation, Shells node and choose Add New Item.

  2. In the Add New Item dialog box, expand the Code node and choose Class.

  3. In the Name field, enter Converters, and then choose the Add button.

  4. Replace the contents with the following code.

    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Media;
    
    namespace ShellExtension.Presentation.Shells
    {
        using Microsoft.LightSwitch;
        using Microsoft.LightSwitch.Details;
        using Microsoft.LightSwitch.Client;
        using Microsoft.LightSwitch.Details.Client;
    
        public class WorkspaceDirtyConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                return (bool)value ? Visibility.Visible : Visibility.Collapsed;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                throw new NotSupportedException();
            }
        }
    
        public class ScreenHasErrorsConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                return (bool)value ? Visibility.Visible : Visibility.Collapsed;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                throw new NotSupportedException();
            }
        }
    
        public class ScreenResultsConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                ValidationResults   results = (ValidationResults)value;
                StringBuilder       sb      = new StringBuilder();
    
                foreach(ValidationResult result in results.Errors)
                    sb.AppendLine(String.Format("Error: {0}", result.Message));
    
                return sb.ToString();
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                throw new NotSupportedException();
            }
        }
    
        public class CurrentUserConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                string  currentUser = (string)value;
    
                if ( (null == currentUser) || (0 == currentUser.Length) )
                    return "Authentication is not enabled.";
    
                return currentUser;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                throw new NotSupportedException();
            }
        }
    }
    

When you write a shell, you must implement many pieces of functionality and you will have to use several systems in the LightSwitch run time. The first step is to update the default shell implementation in the code-behind file.

The shell template provides a starting point for creating your shell extension. You will want to expand upon the basic implementation to define the functionality that your shell will provide.

To update the shell implementation

  1. In Solution Explorer, in the ShellExtension.Client project, choose the Presentation, Shells folder, and then open the ShellSample.xaml.vb or ShellSample.xaml.cs file.

  2. Replace the Imports or using statements with the following.

    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Linq;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
    using System.Windows.Threading;
    using Microsoft.VisualStudio.ExtensibilityHosting;
    using Microsoft.LightSwitch.Sdk.Proxy;
    using Microsoft.LightSwitch.Runtime.Shell;
    using Microsoft.LightSwitch.Runtime.Shell.View;
    using Microsoft.LightSwitch.Runtime.Shell.ViewModels.Commands;
    using Microsoft.LightSwitch.Runtime.Shell.ViewModels.Navigation;
    using Microsoft.LightSwitch.Runtime.Shell.ViewModels.Notifications;
    using Microsoft.LightSwitch.BaseServices.Notifications;
    using Microsoft.LightSwitch.Client;
    using Microsoft.LightSwitch.Framework.Client;
    using Microsoft.LightSwitch.Threading;
    using Microsoft.LightSwitch.Details;
    
  3. Replace the code inside the Presentation.Shells (Visual Basic) or ShellExtension.Presentation.Shells (C#) namespace with the following.

    Partial Public Class ShellSample
            Inherits UserControl
    
            Private serviceProxyCache As IServiceProxy
            Private weakHelperObjects As List(Of Object) = New List(Of Object)()
            Private doubleClickTimer As DispatcherTimer
    
            Public Sub New()
                InitializeComponent()
    
                ' Use the notification service, found on the service proxy, to subscribe to the ScreenOpened,
                ' ScreenClosed, and ScreenReloaded notifications.
                Me.ServiceProxy.NotificationService.Subscribe(GetType(ScreenOpenedNotification), AddressOf Me.OnScreenOpened)
                Me.ServiceProxy.NotificationService.Subscribe(GetType(ScreenClosedNotification), AddressOf Me.OnScreenClosed)
                Me.ServiceProxy.NotificationService.Subscribe(GetType(ScreenReloadedNotification), AddressOf Me.OnScreenRefreshed)
    
                ' Sign up for the Closing event on the user settings service so we can committ any settings changes.
                AddHandler Me.ServiceProxy.UserSettingsService.Closing, AddressOf Me.OnSettingsServiceClosing
    
                ' Read in the saved settings from the user settings service.  This shell saves the width of
                ' the two columns that are separated by a grid splitter.
                Dim width1 As Double = Me.ServiceProxy.UserSettingsService.GetSetting(Of Double)("RootColumn1_Size")
                Dim width2 As Double = Me.ServiceProxy.UserSettingsService.GetSetting(Of Double)("RootColumn2_Size")
    
                ' If the settings were successfully retrieved, then set the column widths accordingly.
                If width1 <> 0 Then
                    Me.LayoutRoot.ColumnDefinitions(0).Width = New GridLength(width1, GridUnitType.Star)
                End If
    
                If width2 <> 0 Then
                    Me.LayoutRoot.ColumnDefinitions(1).Width = New GridLength(width2, GridUnitType.Star)
                End If
    
                ' Initialize the double-click timer (which is used for managing double clicks on an item in the tree view).
                Me.doubleClickTimer = New DispatcherTimer()
                Me.doubleClickTimer.Interval = New TimeSpan(0, 0, 0, 0, 200)
                AddHandler Me.doubleClickTimer.Tick, AddressOf Me.OnDoubleClickTimerTick
            End Sub
    
            Public Sub OnSettingsServiceClosing(sender As Object, e As EventArgs)
                ' This function will get called when the settings service is closing, which happens
                ' when the application is shut down.  In response to that event we will save the
                ' current widths of the two columns.
                Me.ServiceProxy.UserSettingsService.SetSetting("RootColumn1_Size", Me.LayoutRoot.ColumnDefinitions(0).ActualWidth)
                Me.ServiceProxy.UserSettingsService.SetSetting("RootColumn2_Size", Me.LayoutRoot.ColumnDefinitions(1).ActualWidth)
            End Sub
    
            Public Sub OnScreenOpened(n As Notification)
                ' This method is called when a screen has been opened by the runtime.  In response to
                ' this, we need to create a tab item and set its content to be the UI for the newly
                ' opened screen.
                Dim screenOpenedNotification As ScreenOpenedNotification = n
                Dim screenObject As IScreenObject = screenOpenedNotification.Screen
                Dim view As IScreenView = Me.ServiceProxy.ScreenViewService.GetScreenView(screenObject)
    
                ' Create a tab item and bind its header to the display name of the screen.
                Dim ti As TabItem = New TabItem()
                Dim template As DataTemplate = Me.Resources("TabItemHeaderTemplate")
                Dim element As UIElement = template.LoadContent()
    
                ' The IScreenObject does not contain properties indicating if the screen has
                ' changes or validation errors.  As such, we have created a thin wrapper around the
                ' screen object that does expose this functionality.  This wrapper, a class called
                ' MyScreenObject, is what we'll use as the data context for the tab item.
                ti.DataContext = New MyScreenObject(screenObject)
                ti.Header = element
                ti.HeaderTemplate = template
                ti.Content = view.RootUI
    
                ' Add the tab item to the tab control.
                Me.ScreenArea.Items.Add(ti)
                Me.ScreenArea.SelectedItem = ti
    
                ' Set the currently active screen in the active screens view model.
                Me.ServiceProxy.ActiveScreensViewModel.Current = screenObject
            End Sub
    
            Public Sub OnScreenClosed(n As Notification)
                ' A screen has been closed and therefore removed from the application's
                ' collection of active screens.  In response to this, we need to do
                ' two things:
                '  1.  Remove the tab item that was displaying this screen.
                '  2.  Set the "current" screen to the screen that will be displayed
                '      in the tab item that will be made active.
                Dim screenClosedNotification As ScreenClosedNotification = n
                Dim screenObject As IScreenObject = screenClosedNotification.Screen
    
                For Each ti As TabItem In Me.ScreenArea.Items
                    ' We need to get the real IScreenObject from the instance of the MyScreenObject.
                    Dim realScreenObject As IScreenObject = CType(ti.DataContext, MyScreenObject).RealScreenObject
    
                    If realScreenObject Is screenObject Then
                        Me.ScreenArea.Items.Remove(ti)
                        Exit For
                    End If
                Next
    
                ' If there are any tab items left, set the current tab to the last one in the list
                ' AND set the current screen to be the screen contained within that tab item.
                Dim count As Integer = Me.ScreenArea.Items.Count
    
                If count > 0 Then
                    Dim ti As TabItem = Me.ScreenArea.Items(count - 1)
    
                    Me.ScreenArea.SelectedItem = ti
                    Me.ServiceProxy.ActiveScreensViewModel.Current = CType(ti.DataContext, MyScreenObject).RealScreenObject
                End If
            End Sub
    
            Public Sub OnScreenRefreshed(n As Notification)
                ' When a screen is refreshed, the runtime actually creates a new IScreenObject
                ' for it and discards the old one.  So in response to this notification what
                ' we need to do is replace the data context for the tab item that contains
                ' this screen with a wrapper (MyScreenObject) for the new IScreenObject instance.
                Dim srn As ScreenReloadedNotification = n
    
                For Each ti As TabItem In Me.ScreenArea.Items
    
                    Dim realScreenObject As IScreenObject = CType(ti.DataContext, MyScreenObject).RealScreenObject
    
                    If realScreenObject Is srn.OriginalScreen Then
                        Dim view As IScreenView = Me.ServiceProxy.ScreenViewService.GetScreenView(srn.NewScreen)
    
                        ti.Content = view.RootUI
                        ti.DataContext = New MyScreenObject(srn.NewScreen)
                        Exit For
                    End If
                Next
            End Sub
    
            Private ReadOnly Property ServiceProxy As IServiceProxy
                Get
                    If Me.serviceProxyCache Is Nothing Then
                        Me.serviceProxyCache = VsExportProviderService.GetExportedValue(Of IServiceProxy)()
                    End If
                    Return Me.serviceProxyCache
                End Get
            End Property
    
            Private Sub GeneralCommandHandler(sender As Object, e As RoutedEventArgs)
                ' This function will get called when the user clicks one of the buttons on
                ' the command panel.  The sender is the button whose data context is the
                ' IShellCommand.
                '
                ' In order to execute the command (asynchronously) we simply call the
                ' ExecuteAsync method on the ExecutableObject property of the command.
                Dim command As IShellCommand = CType(sender, Button).DataContext
    
                command.ExecutableObject.ExecuteAsync()
            End Sub
    
            Private Sub OnTreeViewLoaded(sender As Object, e As RoutedEventArgs)
                Me.ScreenTree.Dispatcher.BeginInvoke(
                    Sub()
                        Dim tv As TreeView = sender
    
                        For Each item As Object In tv.Items
                            Dim tvi As TreeViewItem = tv.ItemContainerGenerator.ContainerFromItem(item)
    
                            tvi.IsExpanded = True
                        Next
                    End Sub)
            End Sub
    
            Private Sub OnTabItemSelectionChanged(sender As Object, e As SelectionChangedEventArgs)
                ' When the user selects a tab item, we need to set the "active" screen
                ' in the ActiveScreensView model.  Doing this causes the commands view
                ' model to be udpated to reflect the commands of the current screen.
                If e.AddedItems.Count > 0 Then
                    Dim selectedItem As TabItem = e.AddedItems(0)
    
                    If selectedItem IsNot Nothing Then
                        Dim screenObject As IScreenObject = CType(selectedItem.DataContext, MyScreenObject).RealScreenObject
    
                        Me.ServiceProxy.ActiveScreensViewModel.Current = screenObject
                    End If
                End If
            End Sub
    
            Private Sub OnClickTabItemClose(sender As Object, e As RoutedEventArgs)
                ' When the user closes a tab, we simply need to close the corresponding
                ' screen object.  The only caveat here is that the call to the Close
                ' method needs to happen on the logic thread for the screen.  To do this
                ' we need to use the Dispatcher object for the screen.
                Dim screenObject As IScreenObject = TryCast(CType(sender, Button).DataContext, IScreenObject)
    
                If screenObject IsNot Nothing Then
                    screenObject.Details.Dispatcher.EnsureInvoke(
                        Sub()
                            screenObject.Close(True)
                        End Sub)
                End If
            End Sub
    
            Private Sub NavigationItemLeftButtonDown(sender As Object, e As MouseButtonEventArgs)
                If Me.doubleClickTimer.IsEnabled Then
                    Me.doubleClickTimer.Stop()
    
                    ' If the item clicked on is a screen item, then open the screen.
                    Dim screen As INavigationScreen = TryCast(CType(sender, TextBlock).DataContext, INavigationScreen)
    
                    If screen IsNot Nothing Then
                        screen.ExecutableObject.ExecuteAsync()
                    End If
                Else
                    Me.doubleClickTimer.Start()
                End If
            End Sub
    
            Private Sub OnDoubleClickTimerTick(sender As Object, e As EventArgs)
                Me.doubleClickTimer.Stop()
            End Sub
    
        End Class
    
    public partial class ShellSample : UserControl
        {
            private IServiceProxy   serviceProxy;
            private List<object>    weakHelperObjects   = new List<object>();
            private DispatcherTimer doubleClickTimer;
    
            public ShellSample()
            {   
                this.InitializeComponent();
    
                // Use the notification service, found on the service proxy, to subscribe to the ScreenOpened,
                // ScreenClosed, and ScreenReloaded notifications.
                this.ServiceProxy.NotificationService.Subscribe(typeof(ScreenOpenedNotification), this.OnScreenOpened);
                this.ServiceProxy.NotificationService.Subscribe(typeof(ScreenClosedNotification), this.OnScreenClosed);
                this.ServiceProxy.NotificationService.Subscribe(typeof(ScreenReloadedNotification), this.OnScreenRefreshed);
    
                // Sign up for the Closing event on the user settings service so we can commit any settings changes.
                this.ServiceProxy.UserSettingsService.Closing += this.OnSettingsServiceClosing;
    
                // Read in the saved settings from the user settings service.  This shell saves the width of
                // the two columns that are separated by a grid splitter.
                double  width1 = this.ServiceProxy.UserSettingsService.GetSetting<double>("RootColumn1_Size");
                double  width2 = this.ServiceProxy.UserSettingsService.GetSetting<double>("RootColumn2_Size");
    
                // If the settings were successfully retrieved, then set the column widths accordingly.
                if ( width1 != default(double) )
                    this.LayoutRoot.ColumnDefinitions[0].Width = new GridLength(width1, GridUnitType.Star);
    
                if ( width2 != default(double) )
                    this.LayoutRoot.ColumnDefinitions[1].Width = new GridLength(width2, GridUnitType.Star);
    
                // Initialize the double-click timer, which is used for managing double clicks on an item in the tree view.
                this.doubleClickTimer               = new DispatcherTimer();
                this.doubleClickTimer.Interval      = new TimeSpan(0, 0, 0, 0, 200);
                this.doubleClickTimer.Tick          += new EventHandler(this.OnDoubleClickTimerTick);
            }
    
            public void OnSettingsServiceClosing(object sender, EventArgs e)
            {
                // This function will get called when the settings service is closing, which happens
                // when the application is shut down.  In response to that event we will save the
                // current widths of the two columns.
                this.ServiceProxy.UserSettingsService.SetSetting("RootColumn1_Size", this.LayoutRoot.ColumnDefinitions[0].ActualWidth);
                this.ServiceProxy.UserSettingsService.SetSetting("RootColumn2_Size", this.LayoutRoot.ColumnDefinitions[1].ActualWidth);
            }
    
            public void OnScreenOpened(Notification n)
            {
                // This method is called when a screen has been opened by the run time.  In response to
                // this, we need to create a tab item and set its content to be the UI for the newly
                // opened screen.
                ScreenOpenedNotification    screenOpenedNotification    = (ScreenOpenedNotification)n;
                IScreenObject               screenObject                = screenOpenedNotification.Screen;
                IScreenView                 view                        = this.ServiceProxy.ScreenViewService.GetScreenView(screenObject);
    
                // Create a tab item and bind its header to the display name of the screen.
                TabItem         ti          = new TabItem();
                DataTemplate    template    = (DataTemplate)this.Resources["TabItemHeaderTemplate"];
                UIElement       element     = (UIElement)template.LoadContent();
    
                // The IScreenObject does not contain properties indicating if the screen has
                // changes or validation errors.  As such, we have created a thin wrapper around the
                // screen object that does expose this functionality.  This wrapper, a class called
                // MyScreenObject, is what we'll use as the data context for the tab item.
                ti.DataContext      = new MyScreenObject(screenObject);
                ti.Header           = element;
                ti.HeaderTemplate   = template;
                ti.Content          = view.RootUI;
    
                // Add the tab item to the tab control.
                this.ScreenArea.Items.Add(ti);
                this.ScreenArea.SelectedItem = ti;
    
                // Set the currently active screen in the active screens view model.
                this.ServiceProxy.ActiveScreensViewModel.Current = screenObject;
            }
    
            public void OnScreenClosed(Notification n)
            {
                // A screen has been closed and therefore removed from the application's
                // collection of active screens.  In response to this, we need to do
                // two things:
                //  1.  Remove the tab item that was displaying this screen.
                //  2.  Set the "current" screen to the screen that will be displayed
                //      in the tab item that will be made active.
                ScreenClosedNotification    screenClosedNotification    = (ScreenClosedNotification)n;
                IScreenObject               screenObject                = screenClosedNotification.Screen;
    
                foreach(TabItem ti in this.ScreenArea.Items)
                {
                    // We need to get the real IScreenObject from the instance of the MyScreenObject.
                    IScreenObject   realScreenObject = ((MyScreenObject)ti.DataContext).RealScreenObject;
    
                    if ( realScreenObject == screenObject )
                    {
                        this.ScreenArea.Items.Remove(ti);
                        break;
                    }
                }
    
                // If there are any tab items left, set the current tab to the last one in the list
                // AND set the current screen to be the screen contained within that tab item.
                int count = this.ScreenArea.Items.Count;
    
                if ( count > 0 )
                {
                    TabItem ti = (TabItem)this.ScreenArea.Items[count - 1];
    
                    this.ScreenArea.SelectedItem = ti;
                    this.ServiceProxy.ActiveScreensViewModel.Current = ((MyScreenObject)(ti.DataContext)).RealScreenObject;
                }
            }
    
            public void OnScreenRefreshed(Notification n)
            {
                // When a screen is refreshed, the run time actually creates a new IScreenObject
                // for it and discards the old one.  So in response to this notification, 
                // replace the data context for the tab item that contains
                // this screen with a wrapper (MyScreenObject) for the new IScreenObject instance.
                ScreenReloadedNotification  srn = (ScreenReloadedNotification)n;
    
                foreach(TabItem ti in this.ScreenArea.Items)
                {
                    IScreenObject   realScreenObject = ((MyScreenObject)ti.DataContext).RealScreenObject;
    
                    if ( realScreenObject == srn.OriginalScreen )
                    {
                        IScreenView view = this.ServiceProxy.ScreenViewService.GetScreenView(srn.NewScreen);
    
                        ti.Content      = view.RootUI;
                        ti.DataContext  = new MyScreenObject(srn.NewScreen);
                        break;
                    }
                }
            }
    
            private IServiceProxy ServiceProxy
            {
                get
                {
                    // Get the service proxy that provides access to the needed LightSwitch services.
                    if ( null == this.serviceProxy )
                        this.serviceProxy = VsExportProviderService.GetExportedValue<IServiceProxy>();
    
                    return this.serviceProxy;
                }
            }
    
            private void GeneralCommandHandler(object sender, RoutedEventArgs e)
            {
                // This function will get called when the user clicks one of the buttons on
                // the command panel.  The sender is the button whose data context is the
                // IShellCommand.
                //
                // In order to execute the command (asynchronously) we simply call the
                // ExecuteAsync method on the ExecutableObject property of the command.
                IShellCommand   command = (IShellCommand)((Button)sender).DataContext;
    
                command.ExecutableObject.ExecuteAsync();
            }
    
            private void OnTreeViewLoaded(object sender, RoutedEventArgs e)
            {
                this.ScreenTree.Dispatcher.BeginInvoke(() =>
                {
                    TreeView    tv = (TreeView)sender;
    
                    foreach(object item in tv.Items)
                    {
                        TreeViewItem    tvi = tv.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
    
                        tvi.IsExpanded = true;
                    }
                });
            }
    
            private void OnTabItemSelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                // When the user selects a tab item, set the "active" screen
                // in the ActiveScreensView model.  Doing this causes the commands view
                // model to be udpated to reflect the commands of the current screen.
                if ( e.AddedItems.Count > 0 )
                {
                    TabItem selectedItem = (TabItem)e.AddedItems[0];
    
                    if ( null != selectedItem )
                    {
                        IScreenObject   screenObject = ((MyScreenObject)selectedItem.DataContext).RealScreenObject;
    
                        this.ServiceProxy.ActiveScreensViewModel.Current = screenObject;
                    }
                }
            }
    
            private void OnClickTabItemClose(object sender, RoutedEventArgs e)
            {
                // When the user closes a tab, we simply need to close the corresponding
                // screen object.  The only caveat here is that the call to the Close
                // method needs to happen on the logic thread for the screen.  To do this, 
                // use the Dispatcher object for the screen.
                IScreenObject   screenObject = ((Button)sender).DataContext as IScreenObject;
    
                if ( null != screenObject )
                {
                    screenObject.Details.Dispatcher.EnsureInvoke(() =>
                    {
                        screenObject.Close(true);
                    });
                }
            }
    
            private void NavigationItemLeftButtonDown(object sender, MouseButtonEventArgs e)
            {
                if ( this.doubleClickTimer.IsEnabled )
                {
                    this.doubleClickTimer.Stop();
    
                    // If the item clicked on is a screen item, then open the screen.
                    INavigationScreen   screen = ((TextBlock)sender).DataContext as INavigationScreen;
    
                    if ( null != screen )
                        screen.ExecutableObject.ExecuteAsync();
                }
                else
                    this.doubleClickTimer.Start();
            }
    
            private void OnDoubleClickTimerTick(object sender, EventArgs e)
            {
                this.doubleClickTimer.Stop();
            }
        }
    

LightSwitch provides an object that implements the IServiceProxy interface, which provides access to the necessary LightSwitch services. The following segment of code shows how the IServiceProxy object can be retrieved from MEF by means of the VsExportProviderService static.

private IServiceProxy ServiceProxy
        {
            get
            {
                // Get the service proxy, which provides access to the needed LightSwitch services.
                if ( null == this.serviceProxy )
                    this.serviceProxy = VsExportProviderService.GetExportedValue<IServiceProxy>();

                return this.serviceProxy;
            }
        }

In the constructor for your main control, you will have to subscribe to some notifications that provide some important hook points into the workflow of the LightSwitch run time. These notifications are the ScreenOpenedNotification, ScreenClosedNotification, and ScreenReloadedNofitication. Furthermore, if you want your shell to read and write settings as the sample shell does, then you should also register for the Closing event on the UserSettingsService object. The following segment of code implements the constructor and hooks up the notifications.

public ShellSample()
        {
            this.InitializeComponent();

            // Use the notification service, found on the service proxy, to subscribe to the ScreenOpened,
            // ScreenClosed, and ScreenReloaded notifications.
            this.ServiceProxy.NotificationService.Subscribe(typeof(ScreenOpenedNotification), this.OnScreenOpened);
            this.ServiceProxy.NotificationService.Subscribe(typeof(ScreenClosedNotification), this.OnScreenClosed);
            this.ServiceProxy.NotificationService.Subscribe(typeof(ScreenReloadedNotification), this.OnScreenRefreshed);

            // Sign up for the Closing event on the user settings service so you can commit any settings changes.
            this.ServiceProxy.UserSettingsService.Closing += this.OnSettingsServiceClosing;

            // Read in the saved settings from the user settings service.  This shell saves the width of
            // the two columns that are separated by a grid splitter.
            double width1 = this.ServiceProxy.UserSettingsService.GetSetting<double>("RootColumn1_Size");
            double width2 = this.ServiceProxy.UserSettingsService.GetSetting<double>("RootColumn2_Size");

            // If the settings were successfully retrieved, then set the column widths accordingly.
            if (width1 != default(double))
                this.LayoutRoot.ColumnDefinitions[0].Width = new GridLength(width1, GridUnitType.Star);

            if (width2 != default(double))
                this.LayoutRoot.ColumnDefinitions[1].Width = new GridLength(width2, GridUnitType.Star);

            // Initialize the double-click timer, which is used for managing double clicks on an item in the tree view.
            this.doubleClickTimer = new DispatcherTimer();
            this.doubleClickTimer.Interval = new TimeSpan(0, 0, 0, 0, 200);
            this.doubleClickTimer.Tick += new EventHandler(this.OnDoubleClickTimerTick);
        }

The sample shell uses a standard Silverlight TreeView control to display the screens in their appropriate groupings. The following excerpt from the ShellSample.xaml file implements the Navigation pane.

<!-- Navigation view is a simple tree view whose ItemsSource property is bound to -->
        <!-- the collection returned from the NavigationItems property of the Navigation  -->
        <!-- view model.                                                                  -->
        <controls:TreeView x:Name="ScreenTree" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2"
                  Background="{StaticResource NavShellBackgroundBrush}"
                  ShellHelpers:ComponentViewModelService.ViewModelName="Default.NavigationViewModel"
                  ItemsSource="{Binding NavigationItems}"
                  Loaded="OnTreeViewLoaded">
            <controls:TreeView.ItemTemplate>
                <!-- Each navigation item might have children, so set up the binding to the -->
                <!-- Children property of the INavigationGroup                            -->
                <windows:HierarchicalDataTemplate ItemsSource="{Binding Children}">
                    <!-- Each item in the TreeView is a TextBlock whose text value is bound to the DisplayName property of the INavigationItem. -->
                    <TextBlock Style="{StaticResource TextBlockFontsStyle}" 
                               Text="{Binding DisplayName}" 
                               Foreground="{StaticResource NormalFontBrush}" 
                               MouseLeftButtonDown="NavigationItemLeftButtonDown" />
                </windows:HierarchicalDataTemplate>
            </controls:TreeView.ItemTemplate>
        </controls:TreeView>

The XAML specifies that the data context for the TreeView control be set to the view model identified by the value Default.NavigationViewModel by specifying it as the value to the ComponentViewModelService. The INavigationViewModel interface has a property named NavigationItems which returns an observable collection of INavigationItem objects. The ItemsSource property of the TreeView control is bound to this collection.

Each INavigationItem can be an INavigationScreen, which represents a screen that can be executed, or an INavigationGroup, which represents a container of other INavigationItem objects. Therefore, the item template for the tree view control is bound to the Children property and each child is a simple TextBlock control whose Text property is set to the DisplayName property of the INavigationItem object. It is important to notice that screens that cannot be executed by the shell, for example, parameterized screens or screens to which the user has no access, will be filtered out by the NavigationViewModel.

When the user double-clicks on a screen navigation item in the Navigation pane of the sample shell, the screen will be opened in a tab in a tab control. The XAML for the TextBlock control that is used to display each navigation item specifies a handler for the MouseLeftButtonDown event, and it is in this handler that the logic for opening a screen resides. The following segment of code from the ShellSample class adds the handler.

private void NavigationItemLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if ( this.doubleClickTimer.IsEnabled )
            {
                this.doubleClickTimer.Stop();

                // If the item that is clicked is a screen item, then open the screen.
                INavigationScreen   screen = ((TextBlock)sender).DataContext as INavigationScreen;

                if ( null != screen )
                    screen.ExecutableObject.ExecuteAsync();
            }
            else
                this.doubleClickTimer.Start();
        }
private void OnDoubleClickTimerTick(object sender, EventArgs e)
        {
            this.doubleClickTimer.Stop();
        } 

The data context of the TextBlock control is an instance of the INavigationItem interface. If this object can successfully be cast to an instance of the INavigationScreen interface, then the screen should be shown (executed). The ExecutableObject property on the INavigationScreen object contains a method named ExecuteAsync that will cause the LightSwitch run time to load the screen. When the screen is loaded, the ScreenOpenedNotification notification will be published and the handler for it will be called.

The ScreenOpenedNotification object provides access to instances of the IScreenObject and IScreenView interfaces. The IScreenView is how you get the root UI control for the screen. The sample shell takes this control and sets it to be the content of the tab item. Many of the run-time APIs use the IScreenObject in some manner and it would make sense that it would be the data context for the control that owns the screen UI, the tab item in this sample. However, the IScreenObject is missing several key pieces of functionality so this sample includes a helper class named MyScreenObject that provides this functionality and is therefore used as the data context for the tab item.

To create the MyScreenObject class

  1. In Solution Explorer, open the shortcut menu for the Presentation, Shells folder and choose Add, Class.

  2. In the Name field, enter MyScreenObject, and then click Add.

  3. Replace the contents of the class with the following.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Windows;
    
    namespace ShellExtension.Presentation.Shells
    {
        using Microsoft.LightSwitch;
        using Microsoft.LightSwitch.Client;
        using Microsoft.LightSwitch.Details;
        using Microsoft.LightSwitch.Details.Client;
        using Microsoft.LightSwitch.Utilities;
    
        public class MyScreenObject : IScreenObject, INotifyPropertyChanged
        {
            private IScreenObject               screenObject;
            private bool                        dirty;
            private List<IWeakEventListener>    dataServicePropertyChangedListeners;
    
            public event PropertyChangedEventHandler    PropertyChanged;
    
            internal MyScreenObject(IScreenObject screenObject)
            {
                this.screenObject                           = screenObject;
                this.dataServicePropertyChangedListeners    = new List<IWeakEventListener>();
    
                // Register for property changed events on the details object.
                ((INotifyPropertyChanged)screenObject.Details).PropertyChanged += this.OnDetailsPropertyChanged;
    
                // Register for changed events on each of the data services.
                IEnumerable<IDataService>   dataServices = screenObject.Details.DataWorkspace.Details.Properties.All().OfType<IDataWorkspaceDataServiceProperty>().Select(p => p.Value);
    
                foreach(IDataService dataService in dataServices)
                    this.dataServicePropertyChangedListeners.Add(((INotifyPropertyChanged)dataService.Details).CreateWeakPropertyChangedListener(this, this.OnDataServicePropertyChanged));
            }
    
            private void OnDetailsPropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if ( String.Equals(e.PropertyName, "ValidationResults", StringComparison.OrdinalIgnoreCase) )
                {
                    if ( null != this.PropertyChanged )
                        PropertyChanged(this, new PropertyChangedEventArgs("ValidationResults"));
                }
            }
    
            private void OnDataServicePropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                IDataService    dataService = ((IDataServiceDetails)sender).DataService;
    
                this.IsDirty = dataService.Details.HasChanges;
            }
    
            internal IScreenObject RealScreenObject
            {
                get
                {
                    return this.screenObject;
                }
            }
    
            public bool IsDirty
            {
                get
                {
                    return this.dirty;
                }
                set
                {
                    this.dirty = value;
    
                    if ( null != this.PropertyChanged )
                        PropertyChanged(this, new PropertyChangedEventArgs("IsDirty"));
                }
            }
    
            public ValidationResults ValidationResults
            {
                get
                {
                    return this.screenObject.Details.ValidationResults;
                }
            }
    
            public IScreenDetails Details
            {
                get
                {
                    return this.screenObject.Details;
                }
            }
    
            public string Name
            {
                get
                {
                    return this.screenObject.Name;
                }
            }
    
            public string DisplayName
            {
                get
                {
                    return this.screenObject.DisplayName;
                }
                set
                {
                    this.screenObject.DisplayName = value;
                }
            }
    
            public string Description
            {
                get
                {
                    return this.screenObject.Description;
                }
                set
                {
                    this.screenObject.Description = value;
                }
            }
    
            public bool CanSave
            {
                get
                {
                    return this.screenObject.CanSave;
                }
            }
    
            public void Save()
            {
                this.screenObject.Save();
            }
    
            public void Refresh()
            {
                this.screenObject.Refresh();
            }
    
            public void Close(bool promptUserToSave)
            {
                this.screenObject.Close(promptUserToSave);
            }
    
            IBusinessDetails IBusinessObject.Details
            {
                get
                {
                    return ((IBusinessObject)this.screenObject).Details;
                }
            }
    
            IStructuralDetails IStructuralObject.Details
            {
                get
                {
                    return ((IStructuralObject)this.screenObject).Details;
                }
            }
    
            IDetails IObjectWithDetails.Details
            {
                get
                {
                    return ((IObjectWithDetails)this.screenObject).Details;
                }
            }
        }
    }
    

Once screens are opened, it is the responsibility of the shell to keep track of which screen is current. To track the current screen, set the Current property on the ActiveScreensViewModel. To track a screen that is opened, this property is set to the instance of the IScreenObject that has just been opened. The following segment of code from the ShellSample class tracks the open screen.

public void OnScreenOpened(Notification n)
        {
            // This method is called when a screen has been opened by the run time.  In response to
            // this, create a tab item and set its content to be the UI for the newly
            // opened screen.
            ScreenOpenedNotification    screenOpenedNotification    = (ScreenOpenedNotification)n;
            IScreenObject               screenObject                = screenOpenedNotification.Screen;
            IScreenView                 view                        = this.ServiceProxy.ScreenViewService.GetScreenView(screenObject);

            // Create a tab item and bind its header to the display name of the screen.
            TabItem         ti          = new TabItem();
            DataTemplate    template    = (DataTemplate)this.Resources["TabItemHeaderTemplate"];
            UIElement       element     = (UIElement)template.LoadContent();

            // The IScreenObject does not contain properties indicating if the screen has
            // changes or validation errors.  As such, we have created a thin wrapper around the
            // screen object that does expose this functionality.  This wrapper, a class called
            // MyScreenObject, is what you will use as the data context for the tab item.
            ti.DataContext      = new MyScreenObject(screenObject);
            ti.Header           = element;
            ti.HeaderTemplate   = template;
            ti.Content          = view.RootUI;

            // Add the tab item to the tab control.
            this.ScreenArea.Items.Add(ti);
            this.ScreenArea.SelectedItem = ti;

            // Set the currently active screen in the active screens view model.
            this.ServiceProxy.ActiveScreensViewModel.Current = screenObject;
        }

Once screens are open, you must also handle user interactions such as closing or refreshing, or switching between active screens. The notifications for the screen close and screen refresh contain the IScreenObject that is being closed or refreshed. Since the data context for the control that is hosting the screen is actually the MyScreenObject wrapper around the IScreenObject, you must get the underlying screen object instance from the MyScreenObject instance in order to compare it to the screen object that is a part of the notification arguments. The following code retrieves the underlying IScreenObject instance by calling the RealScreenObject property on the MyScreenObject.

IScreenObject   realScreenObject = ((MyScreenObject)ti.DataContext).RealScreenObject;

The basic workflow when closing a screen involves removing the control that is displaying the screen that has closed and then setting the currently active screen to be one of the other screens that are still open, if any are open. In the sample, removing the control involves finding the appropriate tab item and removing it from its tab control parent. Once removed, you must set the Current property on the ActiveScreensViewModel to be the underlying IScreenObject instance that is being wrapped by the MyScreenObject instance serving as the data context for the newly selected tab item. The following segment of code from the ShellSample class handles screen closing.

public void OnScreenClosed(Notification n)
        {
            // A screen has been closed and therefore removed from the application's
            // collection of active screens.  In response to this, do
            // two things:
            //  1.  Remove the tab item that was displaying this screen.
            //  2.  Set the "current" screen to the screen that will be displayed
            //      in the tab item that will be made active.
            ScreenClosedNotification    screenClosedNotification    = (ScreenClosedNotification)n;
            IScreenObject               screenObject                = screenClosedNotification.Screen;

            foreach(TabItem ti in this.ScreenArea.Items)
            {
                // We need to get the real IScreenObject from the instance of the MyScreenObject.
                IScreenObject   realScreenObject = ((MyScreenObject)ti.DataContext).RealScreenObject;

                if ( realScreenObject == screenObject )
                {
                    this.ScreenArea.Items.Remove(ti);
                    break;
                }
            }

            // If there are any tab items left, set the current tab to the last one in the list
            // AND set the current screen to be the screen contained within that tab item.
            int count = this.ScreenArea.Items.Count;

            if ( count > 0 )
            {
                TabItem ti = (TabItem)this.ScreenArea.Items[count - 1];

                this.ScreenArea.SelectedItem = ti;
                this.ServiceProxy.ActiveScreensViewModel.Current = ((MyScreenObject)(ti.DataContext)).RealScreenObject;
            }
        }
private void OnClickTabItemClose(object sender, RoutedEventArgs e)
        {
            // When the user closes a tab, you simply need to close the corresponding
            // screen object.  The only caveat here is that the call to the Close
            // method needs to happen on the logic thread for the screen.  To enable this
            // use the Dispatcher object for the screen.
            IScreenObject screenObject = ((Button)sender).DataContext as IScreenObject;

            if (null != screenObject)
            {
                screenObject.Details.Dispatcher.EnsureInvoke(() =>
                {
                    screenObject.Close(true);
                });
            }
        }

When a screen is refreshed, it is actually torn down and re-executed, which means that the IScreenObject for that screen is a new one. The arguments in the ScreenReloadedNotification notification contain the IScreenObject instance for the previous instance of the screen. This lets you find the control that was hosting the original screen. Once this control is found, its data context must be set to the new IScreenObject for the new screen. For the sample, this means that a new MyScreenObject has to be created that wraps the new IScreenObject and then is placed into the data context for the tab item control. The following segment of code from the ShellSample class handles screen refreshing.

public void OnScreenRefreshed(Notification n)
        {
            // When a screen is refreshed, the run time actually creates a new IScreenObject
            // for it and discards the old one.  So in response to this notification, 
            // you need to replace the data context for the tab item that contains
            // this screen with a wrapper (MyScreenObject) for the new IScreenObject instance.
            ScreenReloadedNotification  srn = (ScreenReloadedNotification)n;

            foreach(TabItem ti in this.ScreenArea.Items)
            {
                IScreenObject   realScreenObject = ((MyScreenObject)ti.DataContext).RealScreenObject;

                if ( realScreenObject == srn.OriginalScreen )
                {
                    IScreenView view = this.ServiceProxy.ScreenViewService.GetScreenView(srn.NewScreen);

                    ti.Content      = view.RootUI;
                    ti.DataContext  = new MyScreenObject(srn.NewScreen);
                    break;
                }
            }
        }
private void OnTabItemSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // When the user selects a tab item, you need to set the "active" screen
            // in the ActiveScreensView model.  Doing this causes the commands view
            // model to be udpated to reflect the commands of the current screen.
            if (e.AddedItems.Count > 0)
            {
                TabItem selectedItem = (TabItem)e.AddedItems[0];

                if (null != selectedItem)
                {
                    IScreenObject screenObject = ((MyScreenObject)selectedItem.DataContext).RealScreenObject;

                    this.ServiceProxy.ActiveScreensViewModel.Current = screenObject;
                }
            }
        }

The sample shell displays the available commands on a command bar across the top of the shell, which is just a standard Silverlight ListBox control. The following excerpt from the ShellSample.xaml file shows the implementation.

<!-- The command panel is a horizontally oriented list box whose data context is set to the  -->
        <!-- CommandsViewModel.  The ItemsSource of this list box is data bound to the ShellCommands -->
        <!-- property.  This results in each item being bound to an instance of an IShellCommand.    -->
        <!--                                                                                         -->
        <!-- The attribute 'ShellHelpers:ComponentViewModelService.ViewModelName' is the manner by   -->
        <!-- which a control specifies the view model that is to be set as its data context.  In     -->
        <!-- case, the view model is identified by the name 'Default.CommandsViewModel'.             -->
        <ListBox x:Name="CommandPanel" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource RibbonBackgroundBrush}"
                        ShellHelpers:ComponentViewModelService.ViewModelName="Default.CommandsViewModel"
                        ItemsSource="{Binding ShellCommands}">

            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <!-- Each item in the list box will be a button whose content is the following:         -->
                    <!--    1.  An image, which is bound to the Image property of the IShellCommand         -->
                    <!--    2.  A text block whose text is bound the DisplayName of the IShellCommand   -->
                    <StackPanel Orientation="Horizontal">
                        <!-- The button will be enabled ordisabled according to the IsEnabled property of  the -->
                        <!-- IShellCommand.  The handler for the click event will execute the command.  -->
                        <Button Click="GeneralCommandHandler"
                                IsEnabled="{Binding IsEnabled}"
                                Style="{x:Null}"
                                Background="{StaticResource ButtonBackgroundBrush}"
                                Margin="1">

                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="32" />
                                    <RowDefinition MinHeight="24" Height="*"/>
                                </Grid.RowDefinitions>
                                <Image Grid.Row="0"
                                       Source="{Binding Image}"
                                       Width="32"
                                       Height="32"
                                       Stretch="UniformToFill"
                                       Margin="0"
                                       VerticalAlignment="Top"
                                       HorizontalAlignment="Center" />
                                <TextBlock Grid.Row="1"
                                           Text="{Binding DisplayName}"
                                           TextAlignment="Center"
                                           TextWrapping="Wrap"
                                           Style="{StaticResource TextBlockFontsStyle}"
                                           MaxWidth="64" />
                            </Grid>
                        </Button>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

In the XAML, notice the data binding to the CommandsViewModel and its various properties by the controls specified in the template. Each command in the ListBox is shown using a Button whose content is an Image control and TextBlock. The IsEnabled property of the button is bound to the IsEnabled property of the IShellCommand, and the Text property of the text block is set to the DisplayName property of the IShellCommand.

In the handler for the Click event for the Button, execute the command by retrieving the ExecuteableObject property on the IShellCommand object and then calling the ExecuteAsync method on it. The following segment of code from the ShellSample class handles screen closing.

private void GeneralCommandHandler(object sender, RoutedEventArgs e)
        {
            // This function will get called when the user clicks one of the buttons on
            // the command panel.  The sender is the button whose data context is the
            // IShellCommand.
            //
            // In order to execute the command (asynchronously), simply call the
            // ExecuteAsync method on the ExecutableObject property of the command.
            IShellCommand   command = (IShellCommand)((Button)sender).DataContext;

            command.ExecutableObject.ExecuteAsync();
        }

In LightSwitch, each screen has its own data workspace. If any of the data in that workspace has changed, then the screen will be considered to be in a dirty state. The IScreenObject does not provide a simple property that tells you if it is dirty or not. Therefore, you have to get this information from the data services that provide data into the screen’s workspace. To do this, you have to register for PropertyChanged events on each data service. The following code excerpts are from the MyScreenObject class, which this sample provides as a wrapper around the IScreenObject instances in order to provide this functionality for you.

// Register for changed events on each of the data services.
        IEnumerable<IDataService>   dataServices = screenObject.Details.DataWorkspace.Details.Properties.All().OfType<IDataWorkspaceDataServiceProperty>().Select(p => p.Value);

        foreach(IDataService dataService in dataServices)
            this.dataServicePropertyChangedListeners.Add(((INotifyPropertyChanged)dataService.Details).CreateWeakPropertyChangedListener(this, this.OnDataServicePropertyChanged));

The OnDataServicePropertyChanged handler checks the HasChanges property on the details object for the data service, as follows.

private void OnDataServicePropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            IDataService    dataService = ((IDataServiceDetails)sender).DataService;

            this.IsDirty = dataService.Details.HasChanges;
        }

In order to show the validation errors for a screen, you must first determine whether the screen has any errors. How to make this determination is like how you determine whether the screen is dirty in that the IScreenObject does not provide a simple property that exposes this state. The details object for the screen contains the validation results and when that result set changes it triggers a PropertyChanged notification. Therefore, you only have to register for this notification. The following code excerpts are from the MyScreenObject class.

// Register for property changed events on the details object.
        ((INotifyPropertyChanged)screenObject.Details).PropertyChanged += this.OnDetailsPropertyChanged;

In the handler for this notification, you have to check if the property being changed on the details object is the ValidationResults property. If it is, then you know that the validation results have changed. In the sample shell, one of the text blocks in the tab item has its Visibility property bound to the ValidationResults property on the MyScreenObject instance. In the handler for the PropertyChanged notification, the PropertyChanged notification is published for the property on the MyScreenObject so that the text indicating that validation errors exist will show in the tab item.

private void OnDetailsPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if ( String.Equals(e.PropertyName, "ValidationResults", StringComparison.OrdinalIgnoreCase) )
            {
                if ( null != this.PropertyChanged )
                    PropertyChanged(this, new PropertyChangedEventArgs("ValidationResults"));
            }
        }

In the sample, hovering over this text, which is just an exclamation mark, will produce a tooltip that displays all the errors for that screen. This is defined in the following excerpt from the ShellSample.xaml file.

<TextBlock Style="{StaticResource TextBlockFontsStyle}" Text="!" 
                   Visibility="{Binding ValidationResults.HasErrors, Converter={StaticResource ScreenHasErrorsConverter}}" 
                   Margin="5, 0, 5, 0" Foreground="Red" FontWeight="Bold">
            <ToolTipService.ToolTip>
                <ToolTip Content="{Binding ValidationResults, Converter={StaticResource ScreenResultsConverter}}" />
            </ToolTipService.ToolTip>
        </TextBlock>

If authentication is enabled in a LightSwitch application, it would be useful to end users if the shell were to display the name of the user who is currently using the application. The sample shell displays this information in the lower-left corner. If authentication is not enabled, then the value “Authentication is not enabled” is shown. The following is the excerpt from the ShellSample.xaml file that defines the controls that display this information.

<!-- The name of the current user is displayed in the lower-left corner of the shell. -->
        <Grid Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <TextBlock Grid.Column="0" Style="{StaticResource TextBlockFontsStyle}" Text="Current User: " Foreground="{StaticResource NormalFontBrush}"/>

            <!-- This TextBlock has its data context set to the CurrentUserViewModel, from which the -->
            <!-- CurrentUserDisplayName property is used to provide the name of the user displayed.  -->
            <TextBlock Grid.Column="1"
                       Style="{StaticResource TextBlockFontsStyle}"
                       ShellHelpers:ComponentViewModelService.ViewModelName="Default.CurrentUserViewModel"
                       Text="{Binding CurrentUserDisplayName, Converter={StaticResource CurrentUserConverter}}"
                       Foreground="{StaticResource NormalFontBrush}"/>
        </Grid>

The current user information is available from the CurrentUserViewModel view model. The text block that shows the current user’s name binds to the CurrentUserDisplayName property on that view model.

The name and description for the shell are defined in the ShellSample.lsml file; the default values are “ShellSample” and “ShellSample description.” These are exposed to the user of your shell in the Application Designer. Therefore, you will want to change both to something more meaningful.

To update the name and description

  1. In Solution Explorer, choose the ShellExtension.Common project.

  2. Expand the Metadata and Shells nodes, and open the ShellSample.lsml file.

  3. In the Shell.Attributes element, replace the DisplayName and Description values, as follows.

    <Shell.Attributes>
          <DisplayName Value="My Sample Shell"/>
          <Description Value="This is my first example of a shell extension."/>
        </Shell.Attributes>
    
    NoteNote

    You can also store these values as resources in the ModuleResources.resx file. For more information, see “To update the control metadata“ in Walkthrough: Creating a Detail Control Extension.

You can test the business type extension in an experimental instance of Visual Studio. If you have not already tested another LightSwitch extensibility project, you have to enable the experimental instance first.

To enable an experimental instance

  1. In Solution Explorer, choose the BusinessTypeExtension.Vsix project.

  2. On the menu bar, choose Project, BusinessTypeExtension.Vsix Properties.

  3. On the Debug tab, under Start Action, choose Start external program.

  4. Enter the path of the Visual Studio executable, devenv.exe.

    By default on a 32-bit system, the path is C:\Program Files\Microsoft Visual Studio 12.0\Common7\IDE\devenv.exe; on a 64-bit system, it is C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\devenv.exe.

  5. In the Command line arguments field, enter /rootsuffix Exp.

    Note Note

    All subsequent LightSwitch extensibility projects will also use this setting, by default.

To test the shell

  1. On the menu bar, choose Debug, Start Debugging. An experimental instance of Visual Studio opens.

  2. In the experimental instance, on the menu bar, choose File, New, Project.

  3. In the Open Project dialog box, select any existing LightSwitch application project, and then choose the OK button.

  4. On the menu bar, choose Project, ProjectName Properties.

  5. In the project designer, on the Extensions tab, select the ShellExtension check box.

  6. Select the General Properties tab and in the Shell list choose My Sample Shell.

  7. On the menu bar, choose Debug, Start Debugging.

    Notice that there is no default screen, and that you have to double-click an item in the Navigation pane in order to open a screen.

This concludes the shell extension walkthrough; you should now have a fully functioning shell extension that you can reuse in any LightSwitch project. This was just one example of a shell extension; you might want to create a shell that is significantly different in behavior or layout. The same basic steps and principles apply to any shell extension, but there are other concepts that apply in other situations.

If you are going to distribute your extension, there are a couple more steps that you will want to take. To make sure that the information displayed for your extension in the Project Designer and in Extension Manager is correct, you must update the properties for the VSIX package. For more information, see How to: Set VSIX Package Properties. In addition, if you are going to distribute your extension publicly, there are several things to consider. For more information, see How to: Distribute a LightSwitch Extension.

Was this page helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft