若要檢視英文版的文章,請選取 [原文] 核取方塊。您也可以將滑鼠指標移到文字上,即可在快顯視窗顯示英文原文。
譯文
原文

逐步解說:建立 Shell 擴充功能

本逐步解說示範如何建立 LightSwitch 的殼層延伸。 LightSwitch 應用程式 Shell 讓使用者能與應用程式互動。 它的作用是 Shell 網域的巡覽項目、執行螢幕、關聯的命令、目前使用者資訊和其他有用的資訊。 當 LightSwitch 提供簡單、強大的 shell 應用程式時,您可以建立會提供您擁有充滿創意的方式,就是使用 LightSwitch 應用程式的各部分的 Shell。

在這個逐步解說中,您會建立類似預設 Shell 的 Shell,不過,在外觀及行為有幾個細微的差異。 命令列排除命令群組並將 [設計螢幕] 按鈕加入至左邊。 巡覽功能表位置是固定的, [啟動] 畫面上不會顯示,而按兩下功能表項目可以開啟畫面。 螢幕實作不同的驗證指示器,且目前的使用者資訊永遠顯示在 Shell 左下角。 這些差異會說明建立殼層延伸的幾個有用的技術。

殼層延伸的結構包含三個主要元件:

  • Managed Extensibility Framework (MEF),匯出 Shell 合約的實作。

  • 可延伸應用程式標記語言 (XAML),描述被 Shell UI 使用的控制項。

  • 在 XAML 後的Visual Basic 或 C# 程式碼,實作控制項行為並與 LightSwitch 執行階段互動。

建立 Shell 擴充功能包含下列工作:

  • Visual Studio 2013 Professional

  • Visual Studio 2013 SDK

  • Visual Studio 2013 的 LightSwitch 擴充性工具組

第一步就是建立專案並加入 [LightSwitch Shell] 範本。

若要建立擴充功能專案

  1. 在 Visual Studio的功能表列,選擇 [ 檔案], 新增專案

  2. 在 [新的專案] 對話方塊中,展開 [Visual Basic] 或 [Visual C#] 節點,然後展開 [LightSwitch] 節點,選取 [擴充性] 節點,然後選取 [LightSwitch 擴充程式庫] 範本。

  3. 在 [名稱] 欄位中,輸入 ShellExtension 做為副檔名程式庫的名字。

  4. 選擇 [] 按鈕以建立包含七個專案對擴充功能所需的方案。

選取副檔名類型

  1. 在 [方案總管] 中,選擇 [ShellExtension.Lspkg] 專案。

  2. 在功能表列中,選擇 [專案]、[加入新項目]。

  3. 在 [加入新項目] 對話方塊中,選擇 [Shell]。

  4. 在 [名稱] 欄位中,輸入 ShellSample 做為副檔名的名字。

  5. 選擇 [確定] 按鈕。 檔案會加入至您方案中的數個專案。

殼層延伸必須參考不是預設範本的某些部分的命名空間。

若要加入參考

  1. 在 [方案總管] 中,開啟 [ShellExtension.客戶] 專案的捷徑功能表,然後選擇 [加入參考]。

  2. 按一下 [加入參考],顯示 [System.Windows.Controls.dll] 對話方塊。

  3. 在 [加入參考]對話方塊,加入參考至 [Microsoft.LightSwitch.ExportProvider.dll] 。

    您可以在 Visual Studio [IDE] 資料夾底下找到 [PrivateAssembly] 資料夾中的組件。

若要實作能被 MEF 使用的 shell ,您必須建立實作 IShell 介面的類別並提供必要的屬性裝飾。 需要的兩個屬性: ExportShell [匯出] 屬性會告知 MEF 哪個合約您的類別會實作,反之,用來區別您的 Shell 實作和其他實作的 Shell 屬性包含中繼資料。 實作由專案範本加入,可以在 [簡介], [Shell], [ShellExtension.Client] 專案的 [元件] 資料夾中找到。 下列範例會示範實作。

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

Shell 屬性指定的資料是您的 Shell 的識別項。 值必須是下列格式: <Module Name>:<Shell Name> 模組的名稱在描述模組的 module.lsml 檔案中指定。 這個檔案和模組名稱是由專案範本所產生的。 Shell 名稱在描述 Shell 的 ProjectName.lsml 檔案中指定。 IShell 介面有兩種方法:傳回 Shell 名稱與在 Shell 屬性指定的值相等,或是傳回 URI 至內嵌資源於建置組件的 XAML 。

當開發 LightSwitch 的殼層延伸,您可以自由地建立並使用任何提供經驗需要的控制項。 每個 LightSwitch 應用程式內容包括一組已知的組件。 下表顯示 LightSwitch 應用程式定義的部分。

組件

模型檢視

描述

巡覽

NavigationViewModel

提供對為開啟螢幕上使用的 [巡覽] 窗格。

命令

CommandsViewModel

提供對為顯示按鈕或其他命令使用的 [命令列] 窗格。

現用螢幕

ActiveScreensViewModel

提供對用來顯示螢幕的 [螢幕] 窗格。

目前的使用者

CurrentUserViewModel

啟用顯示有關目前登入使用者的資訊。

Logo

LogoViewModel

啟用顯示在 Logo 屬性中所指定的影像。

螢幕驗證

ValidationViewModel

提供對驗證 UI。

針對這些組件中的每一個部分, LightSwitch 提供您的控制項可以繫結的檢視模型。LightSwitch 提供讓繫結至這些檢視模型簡單的機制: ComponentViewModelService 當指定為控制項的屬性,這個服務會使用 MEF 尋找指定的檢視模型,具現化並設定它為控制項的資料內容。 下列程式碼範例顯示清單方塊如何設定其資料內容為命令檢視模型。

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

定義 Shell

  1. 在 [方案總管] 中,在 [ShellExtension.Client] 專案中,請選取 [簡介], [Shell] 資料夾,然後開啟 [ShellSample.xaml] 檔案。

  2. 以下列取代內容:

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

    這包含 ShellSample.xaml 檔案的完整程式碼清單;不同部分將在稍後步驟中說明。 您可以忽略有關遺漏型別的任何錯誤;這些之後也將增加。

  3. 在 [方案總管] 中,開啟 [簡介] 的捷徑功能表,在 [ShellExtension.Client] 專案的 [Shell] 節點並選取 [加入新項目。]。

  4. 在 [加入新項目。] 對話方塊中,展開 [Silverlight] 節點,然後選取 [Silverlight 資源字典]。

  5. 在 [名稱] 文字方塊中輸入 TextBlockStyle,然後選擇 [新增] 按鈕。

  6. 使用以下來取代現存 XAML 定義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>
    

    資源字典是由 UserControl XAML 參考並指定套用至所有 TextBlock 控制項的樣式。

Shell 的 XAML 參考多個值轉換器;您將接著定義。

定義值轉換子。

  1. 在 [方案總管] 中,開啟 [簡介] 的捷徑功能表,在 [ShellExtension.Client] 專案的 [Shell] 節點並選取 [加入新項目。]。

  2. 在 [加入新項目。] 對話方塊中,展開 [程式碼] 節點並選取 [類別]。

  3. 在 [名稱] 欄中,輸入轉換子,然後選擇 [新增] 按鈕。

  4. 以下列程式碼取代內容。

    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();
            }
        }
    }
    

當您撰寫應用程式時,您必須實作功能的許多部分,而您必須在 LightSwitch 執行階段內使用數個系統。 第一步是更新程式碼後置檔案的預設 Shell 實作。

Hh290138.collapse_all(zh-tw,VS.140).gif更新預設 Shell 實作

Shell 範本提供您建立 Shell Extension 的起點。 要擴充基底實作定義您的 Shell 中提供的功能。

更新 Shell 實作

  1. 在 [方案總管] 中,在 [ShellExtension.Client] 專案中,請選取 [簡介], [Shell] 資料夾,然後開啟 [ShellSample.xaml.vb] 或 [ShellSample.xaml.cs] 檔。

  2. 以下列內容取代 Importsusing 陳述式。

    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. 以下列程式碼取代 Presentation.Shells (Visual Basic) 內的程式碼或 ShellExtension.Presentation.Shells (C#) 命名空間。

    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 提供實作 IServiceProxy 介面的物件,提供對 LightSwitch 必要的服務。 下列程式碼區段顯示 IServiceProxy 物件如何從 MEF 擷取透過 VsExportProviderService 靜態。

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

在主控制項的建構函式,您必須訂閱提供一些重要攔截點到 LightSwitch 執行階段之工作流程的某些通知。 這些通知是 ScreenOpenedNotificationScreenClosedNotificationScreenReloadedNofitication 此外,如果您想要讓 Shell 讀取和寫入設定如同範例 Shell,則您也應該註冊在 [UserSettingsService] 物件的 Closing 事件。下列程式碼區段實作建構函式和攔截通知。

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);
        }

Hh290138.collapse_all(zh-tw,VS.140).gif顯示可用螢幕

範例 Shell 使用標準 Silverlight TreeView 控制項顯示其適當的群組的螢幕。 下列摘錄 ShellSample.xaml 檔案實作 [巡覽] 窗格。

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

XAML 指定 TreeView 控制項的資料內容設定為值識別的檢視模型傳遞 Default.NavigationViewModel 指定它做為 ComponentViewModelService的值。 INavigationViewModel 介面會傳回 INavigationItem 物件的可預見集合屬性名稱的 NavigationItems TreeView 控制項的 ItemsSource 屬性繫結至這個集合。

每個 INavigationItem 可以是 INavigationScreen,表示可執行的螢幕, INavigationGroup表示容器的 INavigationItem 物件。 因此,樹狀檢視控制項的項目樣板繫結至 Children 屬性,且每個子系是 Text 屬性設定為 INavigationItem 物件的 DisplayName 屬性的間單 TextBlock 控制項。請注意很重要,例如螢幕,無法由 Shell 執行參數型的使用者沒有存取權的螢幕或螢幕,將由 NavigationViewModel篩選。

Hh290138.collapse_all(zh-tw,VS.140).gif若要開啟螢幕

當使用者按兩下範例 Shell 的 [巡覽] 窗格中的螢幕導覽項目,螢幕在索引標籤控制項的索引標籤中開啟。 用來顯示每個巡覽項目的 TextBlock 控制項的 XAML 提供 MouseLeftButtonDown 事件的處理常式和它是在這個處理常式開頭的邏輯螢幕填入。 下列程式碼區段是 ShellSample 類別中加入處理常式。

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();
        } 

TextBlock 控制項的資料內容是 INavigationItem 介面的執行個體。 如果這個物件可以順利轉換為 INavigationScreen 介面的執行個體,則應該顯示螢幕 (被執行)。 INavigationScreen 物件的 ExecutableObject 屬性包含會造成 LightSwitch 執行階段載入螢幕的一個名為的 ExecuteAsync 。當螢幕載入, ScreenOpenedNotification 告知要發行,而其處理常式被呼叫。

ScreenOpenedNotification 物件可用來存取 IScreenObjectIScreenView 介面的執行個體。 IScreenView 是您取得螢幕的根 UI 控制項方法。 範例 Shell 接管並將它設定為索引標籤項目的內容。 許多執行階段 API 會使用 IScreenObject ,因為它是擁有螢幕 UI 的控制項,在這個範例的索引標籤項目的資料內容。 不過, IScreenObject 遺漏功能幾個重要部分,因此這個範例包括提供這項功能的一個 Helper 類別名稱和 MyScreenObject 做為資料內容為索引標籤項目。

建立 MyScreenObject 類別

  1. 在 [方案總管] 中,開啟 [顯示]、[Shell]資料夾的捷徑功能表,然後選擇 [加入]、[類別]。

  2. 在 [名稱] 欄位中輸入 MyScreenObject,然後按一下 [加入]。

  3. 以下列取代類別的內容:

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

一旦開啟畫面, Shell 會責任記錄目前螢幕的軌跡。 若要追蹤目前螢幕,請設定Current 屬性於 ActiveScreensViewModel 若要追蹤開啟的螢幕,這個屬性設定為開啟的 IScreenObject 執行個體。 下列程式碼區段是 ShellSample 類別的追蹤開啟畫面。

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

Hh290138.collapse_all(zh-tw,VS.140).gif處理螢幕互動

一旦螢幕是開啟的,您還必須管理使用者互動 (例如關閉、重新整理或交換現用螢幕之間)。 通知螢幕的關閉,並重新整理螢幕包含關閉或重新整理的 IScreenObject 因為裝載畫面的控制項的資料內容實際上是 MyScreenObject 包裝函式在 IScreenObject內,您必須從 MyScreenObject 執行個體取得基礎螢幕物件執行個體為了配合通知引數的螢幕物件比較。 下列程式碼會呼叫 MyScreenObjectRealScreenObject 屬性會擷取基礎 IScreenObject 執行個體。

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

基本的工作流程,當在螢幕涉及移除顯示螢幕被關閉並設定目前作用中的螢幕是其他螢幕之一是開啟的控制項,如果任何已開啟。 在這個範例中,移除控制項包含找出適當的索引標籤項目和移除它的索引標籤控制項的父代。 一旦移除,您必須設定 ActiveScreensViewModelCurrent 屬性是由 MyScreenObject 執行個體服務包裝成新選取的索引標籤項目的資料內容的基底 IScreenObject 執行個體。 下列程式碼區段從 ShellSample 類別控制代碼的篩選結束。

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);
                });
            }
        }

當重新整理螢幕,它實際上被卸除並重新實作,表示螢幕的 IScreenObject 是新的。 ScreenReloadedNotification 通知的引數包含螢幕的前一個執行個體的 IScreenObject 執行個體。 這可讓您尋找裝載原始畫面的控制項。 一旦找到這個控制項,必須將其資料內容加入新螢幕的新 IScreenObject 對於這個範例,包裝新 IScreenObject 然後放入索引標籤項目控制項的資料內容的這表示新 MyScreenObject 之前建立。 下列程式碼區段從 ShellSample 類別控制代碼的篩選重新整理。

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

Hh290138.collapse_all(zh-tw,VS.140).gif實作命令

範例 Shell 顯示在一個命令列上的可用命令跨 Shell 的頂端,是標準 Silverlight [ListBox] 控制項。 下列摘錄 ShellSample.xaml 檔案顯示實作。

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

在 XAML 中,請注意藉由在範本中指定的控制項資料繫結至 CommandsViewModel 和其不同的屬性。 ListBox 的每個命令顯示使用內容控制項為 ImageTextBlockButton 按鈕的 IsEnabled 屬性繫結至 IShellCommandIsEnabled 屬性,而文字區塊的 Text 屬性設定為 IShellCommandDisplayName 屬性。

Click 事件處理常式的 Button,以擷取 IShellCommand 物件的 ExecuteableObject 屬性會在其上呼叫 ExecuteAsync 方法執行命令。 下列程式碼區段從 ShellSample 類別控制螢幕結束。

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();
        }

Hh290138.collapse_all(zh-tw,VS.140).gif處理資料變更和驗證

在 LightSwitch,每個螢幕都有自己的資料工作區。 如果有任何資料在工作區中已經變更,則螢幕將視為在 變更狀態 不論是否已變更,IScreenObject 未提供會告知您的簡單屬性。 因此,您必須將提供資料至螢幕的工作區的資料服務取得這個資訊。 若要這樣做,您必須在每個資料服務的 PropertyChanged 事件註冊。 下列程式碼區段是自 MyScreenObject 類別,這個範例在 IScreenObject 執行個體周圍提供,但包裝函式來提供這項功能。

// 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));

OnDataServicePropertyChanged 處理常式會檢查 HasChanges 屬性為資料服務詳細資料物件,如下所示。

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

            this.IsDirty = dataService.Details.HasChanges;
        }

若要顯示螢幕的驗證錯誤,您必須先判斷螢幕是否有任何錯誤。 如何進行這項判斷與您如何判斷螢幕是否變更因為 IScreenObject 不提供公開這個狀態的簡單屬性。 詳細資料物件螢幕包含驗證結果,且當該結果集變更會觸發 PropertyChanged 通知。 因此,您只需要註冊這個告知。 下列程式碼區段是自 MyScreenObject 類別。

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

在此通知的處理常式,您必須檢查詳細資料物件已變更的屬性是否為 ValidationResults 屬性。 如果是,則您知道驗證結果已變更。 在範例 Shell,其中一個索引標籤項目的文字區塊將 Visibility 屬性繫結至 MyScreenObject 執行個體的 ValidationResults 屬性。 PropertyChanged 通知的處理常式,告知 PropertyChanged 做為 MyScreenObject 中的屬性中,以便指示文字驗證錯誤存在於選項項目顯示。

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

在範例中,將滑鼠游標停留在此驚嘆號,會產生顯示該螢幕的所有錯誤的工具提示。 這在下列區段定義從 ShellSample.xaml 檔案。

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

Hh290138.collapse_all(zh-tw,VS.140).gif顯示目前使用者

如果驗證在 LightSwitch 應用程式有效,當 Shell 顯示目前使用應用程式的使用者名稱對使用者很有幫助。 範例 Shell 顯示在左下角這項資訊。 如果驗證未啟用,則值「驗證未啟用」顯示。下列摘錄自定義控制項顯示這項資訊的 ShellSample.xaml 檔案。

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

目前使用者資訊從 CurrentUserViewModel 檢視模型中取得。 顯示目前使用者名稱的文字區塊繫結至 CurrentUserDisplayName 在該檢視模型的屬性。

Shell 中的名稱與描述都被定義在 ShellSample.lsml 檔案中;預設值為「ShellSample」和「ShellSample 描述」。這些公開在 [應用程式設計工具] 的 Shell 的使用者。 因此,您會想要變更兩個為更有意義的名稱。

更新名稱和描述

  1. 在 [方案總管] 中,選擇 [ShellExtension.Common] 專案。

  2. 展開 [中繼資料] 和 [Shell] 節點,然後開啟 [ShellSample.lsml] 檔案。

  3. Shell.Attributes 項目,請將 DisplayNameDescription 值取代,如下所示。

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

    在 ModuleResources.resx 的資源檔,您也可以儲存這些值。 如需「更新控制中繼資料」詳細資訊,請參閱 逐步解說:建立詳細資料控制項擴充功能

您可以在 Visual Studio的實驗執行個體中企業型別擴充。 如果您尚未測試其他 LightSwitch 擴充性專案,您必須先啟用實驗執行個體。

隨即啟用實驗執行個體。

  1. 在 [方案總管] 中,選擇 [BusinessTypeExtension.Vsix] 專案。

  2. 選擇功能表列上的 [專案]、[BusinessTypeExtension.Vsix 屬性]。

  3. 在 [偵錯] 索引標籤,請在 [啟動動作] 底下,選取 [啟動外部程式]。

  4. 進入 Visual Studio 可執行檔, devenv.exe 的路徑。

    預設會在 32 位元系統上,路徑是 C:\Program Files\Microsoft Visual Studio 12.0 \ Common7 \ IDE \ devenv.exe;在 64 位元系統上,它是 C:\Program Files (x86) \ Microsoft Visual Studio 12.0 \ Common7 \ IDE \ devenv.exe。

  5. 在 [命令列引數] 欄位中,輸入 /rootsuffix Exp

    注意事項 注意事項

    所有後續的 LightSwitch 擴充性專案也會預設使用這個設定。

若要測試shell

  1. 在功能表列上,選擇 [偵錯]、[開始偵錯]。 Visual Studio 的實驗執行個體隨即開啟。

  2. 在實驗個體功能列表中,選擇 [檔案]、[新增]、[專案]。

  3. 在 [開啟專案] 對話方塊中,選取任何現有的 LightSwitch 應用程式專案,然後選擇 [] 按鈕。

  4. 在功能表列上,選擇 [專案]、ProjectName[Properties]。

  5. 在專案設計工具,請在 [擴充功能] 索引標籤上,選取 [ShellExtension] 核取方塊。

  6. 選取 [一般屬性] 索引標籤,然後在 [Shell] 清單中選擇 [我的範例 Shell]。

  7. 在功能表列上,選擇 [偵錯]、[開始偵錯]。

    請注意沒有預設螢幕,因此,您必須按兩下 [巡覽] 窗格中的項目以開啟畫面。

這個結束殼層延伸逐步解說;現在您應該可以在所有 LightSwitch 專案中重複使用的完整作用的殼層延伸。 這是殼層延伸的一個例子;您可以建立行為或配置不相同的 Shell。 相同的基本步驟及原則適用於所有殼層延伸,但在其他情況套用其他的概念。

如果您散發您的擴充功能,有幾個要採用的詳細步驟。 若要確定您的擴充功能所顯示的資訊在 [專案設計工具] 和 [增益集管理員] 是正確的,您必須更新 VSIX 套件的屬性。 如需詳細資訊,請參閱如何:設定 VSIX 套件屬性 此外,如果您公開散發您的擴充功能,請考慮幾點。 如需詳細資訊,請參閱如何:散發 LightSwitch 擴充功能

顯示: