Information
The topic you requested is included in another documentation set. For convenience, it's displayed below. Choose Switch to see the topic in its original location.

Walkthrough: Creating a Shell Extension

This walkthrough demonstrates how to create a shell extension for LightSwitch. The shell for a Visual Studio LightSwitch 2011 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 2010 SP1 (Professional, Premium, or Ultimate edition)

  • Visual Studio 2010 SP1 SDK

  • Visual Studio LightSwitch 2011

  • Visual Studio LightSwitch 2011 Extensibility Toolkit

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, select the LightSwitch node, and then select LightSwitch Extension Library (Visual Basic) or LightSwitch Extension Library (C#).

  3. In the Name field, type 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, select the ShellExtension.Lspkg project.

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

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

  4. In the Name field, type 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 then 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(GetType(IShell))>
    <Shell(ShellSample.ShellId)>
    Friend Class ShellSample
        Implements 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 then 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, type 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 then choose Add New Item.

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

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

  4. Replace the contents with the following code.

    Imports System
    Imports System.Collections.Generic
    Imports System.Globalization
    Imports System.Linq
    Imports System.Text
    Imports System.Windows
    Imports System.Windows.Data
    Imports System.Windows.Media
    Imports Microsoft.LightSwitch
    Imports Microsoft.LightSwitch.Details
    Imports Microsoft.LightSwitch.Client
    Imports Microsoft.LightSwitch.Details.Client
    
    Namespace Presentation.Shells
    
        Public Class WorkspaceDirtyConverter
            Implements IValueConverter
    
            Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
                Return If(CType(value, Boolean), Visibility.Visible, Visibility.Collapsed)
            End Function
    
            Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
                Throw New NotSupportedException()
            End Function
    
        End Class
    
        Public Class ScreenHasErrorsConverter
            Implements IValueConverter
    
            Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
                Return If(CType(value, Boolean), Visibility.Visible, Visibility.Collapsed)
            End Function
    
            Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
                Throw New NotSupportedException()
            End Function
    
        End Class
    
        Public Class ScreenResultsConverter
            Implements IValueConverter
    
            Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
                Dim results As ValidationResults = value
                Dim sb As StringBuilder = New StringBuilder()
    
                For Each result As ValidationResult In results.Errors
                    sb.Append(String.Format("Errors: {0}", result.Message))
                Next
    
                Return sb.ToString()
            End Function
    
            Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
                Throw New NotSupportedException()
            End Function
    
        End Class
    
        Public Class CurrentUserConverter
            Implements IValueConverter
    
            Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
                Dim currentUser As String = value
    
                If currentUser Is Nothing OrElse currentUser.Length = 0 Then
                    Return "Authentication is not enabled."
                End If
    
                Return currentUser
            End Function
    
            Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
                Throw New NotSupportedException()
            End Function
    
        End Class
    
    End Namespace
    

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.

Updating the Default Shell Implementation

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.

    Imports System
    Imports System.Collections.Generic
    Imports System.Collections.Specialized
    Imports System.Linq
    Imports System.Net
    Imports System.Windows
    Imports System.Windows.Controls
    Imports System.Windows.Data
    Imports System.Windows.Documents
    Imports System.Windows.Input
    Imports System.Windows.Media
    Imports System.Windows.Media.Animation
    Imports System.Windows.Media.Imaging
    Imports System.Windows.Shapes
    Imports System.Windows.Threading
    Imports Microsoft.VisualStudio.ExtensibilityHosting
    Imports Microsoft.LightSwitch.Sdk.Proxy
    Imports Microsoft.LightSwitch.Runtime.Shell
    Imports Microsoft.LightSwitch.Runtime.Shell.View
    Imports Microsoft.LightSwitch.Runtime.Shell.ViewModels.Commands
    Imports Microsoft.LightSwitch.Runtime.Shell.ViewModels.Navigation
    Imports Microsoft.LightSwitch.Runtime.Shell.ViewModels.Notifications
    Imports Microsoft.LightSwitch.BaseServices.Notifications
    Imports Microsoft.LightSwitch.Client
    Imports Microsoft.LightSwitch.Framework.Client
    Imports Microsoft.LightSwitch.Threading
    Imports 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 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

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

To Show the Available Screens

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.

To Open a Screen

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

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, type MyScreenObject, and then click Add.

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

    Imports System
    Imports System.Collections.Generic
    Imports System.ComponentModel
    Imports System.Linq
    Imports System.Windows
    
    Imports Microsoft.LightSwitch
    Imports Microsoft.LightSwitch.Client
    Imports Microsoft.LightSwitch.Details
    Imports Microsoft.LightSwitch.Details.Client
    Imports Microsoft.LightSwitch.Utilities
    
    Namespace Presentation.Shells
    
        Public Class MyScreenObject
            Implements IScreenObject
            Implements INotifyPropertyChanged
    
            Private screenObject As IScreenObject
            Private dirty As Boolean
            Private dataServicePropertyChangedListeners As List(Of IWeakEventListener)
    
            Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
    
            Friend Sub New(screenObject As IScreenObject)
                Me.screenObject = screenObject
                Me.dataServicePropertyChangedListeners = New List(Of IWeakEventListener)
    
                ' Register for property changed events on the details object.
                AddHandler CType(screenObject.Details, INotifyPropertyChanged).PropertyChanged, AddressOf Me.OnDetailsPropertyChanged
    
                ' Register for changed events on each of the data services.
                Dim dataServices As IEnumerable(Of IDataService) =
                    screenObject.Details.DataWorkspace.Details.Properties.All().OfType(Of IDataWorkspaceDataServiceProperty)().Select(Function(p) p.Value)
    
                For Each dataService As IDataService In dataServices
                    Me.dataServicePropertyChangedListeners.Add(CType(dataService.Details, INotifyPropertyChanged).CreateWeakPropertyChangedListener(Me, AddressOf Me.OnDataServicePropertyChanged))
                Next
            End Sub
    
            Private Sub OnDetailsPropertyChanged(sender As Object, e As PropertyChangedEventArgs)
                If String.Equals(e.PropertyName, "ValidationResults", StringComparison.OrdinalIgnoreCase) Then
                    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("ValidationResults"))
                End If
            End Sub
    
            Private Sub OnDataServicePropertyChanged(sender As Object, e As PropertyChangedEventArgs)
                Dim dataService As IDataService = CType(sender, IDataServiceDetails).DataService
                Me.IsDirty = dataService.Details.HasChanges
            End Sub
    
            Friend ReadOnly Property RealScreenObject As IScreenObject
                Get
                    Return Me.screenObject
                End Get
            End Property
    
            Public Property IsDirty As Boolean
                Get
                    Return Me.dirty
                End Get
                Set(value As Boolean)
                    Me.dirty = value
                    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("IsDirty"))
                End Set
            End Property
    
            Public ReadOnly Property ValidationResults As ValidationResults
                Get
                    Return Me.screenObject.Details.ValidationResults
                End Get
            End Property
    
            Public ReadOnly Property CanSave As Boolean Implements IScreenObject.CanSave
                Get
                    Return Me.screenObject.CanSave
                End Get
            End Property
    
            Public Sub Close(promptUserToSave As Boolean) Implements IScreenObject.Close
                Me.screenObject.Close(promptUserToSave)
            End Sub
    
            Public Property Description As String Implements IScreenObject.Description
                Get
                    Return Me.screenObject.Description
                End Get
                Set(value As String)
                    Me.screenObject.Description = value
                End Set
            End Property
    
            Public ReadOnly Property Details As IScreenDetails Implements IScreenObject.Details
                Get
                    Return Me.screenObject.Details
                End Get
            End Property
    
            Public Property DisplayName As String Implements IScreenObject.DisplayName
                Get
                    Return Me.screenObject.DisplayName
                End Get
                Set(value As String)
                    Me.screenObject.DisplayName = value
                End Set
            End Property
    
            Public ReadOnly Property Name As String Implements IScreenObject.Name
                Get
                    Return Me.screenObject.Name
                End Get
            End Property
    
            Public Sub Refresh() Implements IScreenObject.Refresh
                Me.screenObject.Refresh()
            End Sub
    
            Public Sub Save() Implements IScreenObject.Save
                Me.screenObject.Save()
            End Sub
    
            Public ReadOnly Property Details1 As IBusinessDetails Implements IBusinessObject.Details
                Get
                    Return CType(Me.screenObject, IBusinessObject).Details
                End Get
            End Property
    
            Public ReadOnly Property Details2 As IDetails Implements IObjectWithDetails.Details
                Get
                    Return CType(Me.screenObject, IObjectWithDetails).Details
                End Get
            End Property
    
            Public ReadOnly Property Details3 As IStructuralDetails Implements IStructuralObject.Details
                Get
                    Return CType(Me.screenObject, IStructuralObject).Details
                End Get
            End Property
        End Class
    
    End Namespace
    

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

Handling Screen Interaction

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.

Dim realScreenObject As IScreenObject = DirectCast(ti.DataContext, MyScreenObject).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 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

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

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

Implement Commands

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

Handle Data Changes and Validation

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.
            Dim dataServices As IEnumerable(Of IDataService) =
                screenObject.Details.DataWorkspace.Details.Properties.All().OfType(Of IDataWorkspaceDataServiceProperty)().Select(Function(p) p.Value)

            For Each dataService As IDataService In dataServices
                Me.dataServicePropertyChangedListeners.Add(CType(dataService.Details, INotifyPropertyChanged).CreateWeakPropertyChangedListener(Me, AddressOf Me.OnDataServicePropertyChanged))
            Next

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

Private Sub OnDataServicePropertyChanged(sender As Object, e As PropertyChangedEventArgs)
            Dim dataService As IDataService = CType(sender, IDataServiceDetails).DataService
            Me.IsDirty = dataService.Details.HasChanges
        End Sub

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.
            AddHandler CType(screenObject.Details, INotifyPropertyChanged).PropertyChanged, AddressOf Me.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 Sub OnDetailsPropertyChanged(sender As Object, e As PropertyChangedEventArgs)
            If String.Equals(e.PropertyName, "ValidationResults", StringComparison.OrdinalIgnoreCase) Then
                RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("ValidationResults"))
            End If
        End Sub

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>

Display the Current User

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, select the ShellExtension.Vsix project.

  2. On the menu bar, choose Project, ShellExtension.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 10.0\Common7\IDE\devenv.exe; on a 64-bit system, it is C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\devenv.exe.

  5. In the Command line arguments field, type /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, Open 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

Community Additions

Show:
© 2014 Microsoft