Tutorial: Crear una extensión de shell

Este tutorial muestra cómo crear una extensión de shell para LightSwitch. El shell para una aplicación de LightSwitch permite a los usuarios interactuar con la aplicación. Manifiesta los elementos navegables, pantallas de ejecución, comandos asociados, información del usuario actual y otra información útil que forman parte del dominio del shell. Aunque LightSwitch proporcione un shell sencillo y eficaz, puede crear su propio shell que le proporcionará sus propios medios creativos para interactuar con las distintas partes de la aplicación de LightSwitch.

En este tutorial se crea un shell que se parece al shell predeterminado, pero con varias diferencias imperceptibles en apariencia y comportamiento. La barra de comandos elimina los grupos de comando y mueve el botón Pantalla de diseño a la izquierda. El menú de navegación se corrige en la posición, no se muestra la pantalla Inicio y las pantallas se abren haciendo doble clic en los elementos de menú. Las pantallas implementan un indicador de validaciones diferente y siempre se muestra la información del usuario actual en la esquina inferior izquierda del shell. Estas diferencias ayudarán a ilustrar varias técnicas útiles para crear extensiones de shell.

La anatomía de una extensión de shell se compone de tres partes principales:

  • Managed Extensibility Framework (MEF), que exporta la implementación del contrato de shell.

  • Lenguaje XAML (Extensible Application Markup Language), que describe los controles que se utilizan para el shell de interfaz de usuario.

  • El código de Visual Basic o C# detrás de XAML, que implementa el comportamiento de los controles e interactúa con el tiempo de ejecución de LightSwitch.

La creación de una extensión de shell implica las siguientes tareas:

  • Crear un proyecto de extensión de shell.

  • Agregar referencias a espacios de nombres.

  • Crear el elemento MEF.

  • Definir el shell.

  • Implementar el shell.

  • Proporcionar un nombre descriptivo y una descripción.

  • Probar la extensión de shell.

Requisitos previos

  • Visual Studio 2013 Professional

  • Visual Studio 2013 SDK

  • LightSwitch Extensibility Toolkit para Visual Studio 2013

Crear un proyecto de extensión de shell

El primer paso es crear un proyecto y agregar una plantilla Shell de LightSwitch.

Para crear un proyecto de extensión

  1. En la barra de menús de Visual Studio, elija Archivo, Nuevo, Proyecto.

  2. En el cuadro de diálogo Nuevo proyecto, expanda el nodo Visual Basic o el nodo Visual C#, expanda el nodo LightSwitch, elija el nodo Extensibilidad y, a continuación, elija la plantilla Biblioteca de extensión de LightSwitch.

  3. En el campo Nombre, escriba ShellExtension como nombre de la biblioteca de extensión.

  4. Elija el botón Aceptar para crear una solución que contenga los siete proyectos que son necesarios para la extensión.

Para elegir un tipo de extensión

  1. En el Explorador de soluciones, elija el proyecto ShellExtension.Lspkg.

  2. En la barra de menús, elija Proyecto, Agregar nuevo elemento.

  3. En el cuadro de diálogo Agregar nuevo elemento, elija Shell.

  4. En campo Nombre, escriba ShellSample como nombre de la extensión.

  5. Elija el botón Aceptar. Los archivos se agregarán a varios proyectos de la solución.

Agregar referencias

La extensión de shell tendrá que hacer referencia a algunos espacios de nombres que no forman parte de la plantilla predeterminada.

Para agregar referencias

  1. En el Explorador de soluciones, abra el menú contextual del proyecto ShellExtension.Client y luego elija Agregar referencia.

  2. En el cuadro de diálogo Agregar referencia, agregue una referencia a System.Windows.Controls.dll.

  3. En el cuadro de diálogo Agregar referencia, agregue una referencia a Microsoft.LightSwitch.ExportProvider.dll.

    Puede encontrar el ensamblado en la carpeta PrivateAssembly bajo la carpeta IDE de Visual Studio.

Crear el elemento MEF

Para que una implementación de un shell esté disponible para MEF, se tiene que crear una clase que implementa la interfaz IShell y proporciona las decoraciones de atributo necesarias. Hay dos atributos de este tipo que son necesarios: Export y Shell. El atributo Exportar indica a MEF el contrato que implementa la clase, mientras que el atributo Shell contiene los metadatos que se usan para distinguir la implementación de un shell de otras implementaciones. La plantilla de proyecto agrega la implementación y se puede encontrar en la carpeta Presentación, Shells, Componentes del proyecto ShellExtension.Client. En el siguiente ejemplo se muestra la implementación.

<Export(GetType(IShell))>
    <Shell(ShellSample.ShellId)>
    Friend Class ShellSample
        Implements IShell
[Export(typeof(IShell))]
    [Shell(ShellSample.ShellId)]
    internal class ShellSample : IShell
    {
     ...
    }

Los datos especificado en el atributo Shell son el identificador del shell. El valor debe tener el formato siguiente: <Module Name>:<Shell Name>. El nombre del módulo se especifica en el archivo module.lsml que describe el módulo. La plantilla de proyecto genera este archivo y el nombre del módulo. El nombre del shell se especifica en el archivo ProjectName.lsml que describe el shell. La interfaz IShell tiene dos métodos: uno que devuelve el nombre del shell, que es equivalente al valor especificado en el atributo Shell y otro que devuelve un URI al XAML, que es un recurso incrustado en el ensamblado compilado.

Definir el shell

Al desarrollar una extensión de shell para LightSwitch, se tiene la libertad para crear y utilizar cualquier control que proporciona la experiencia que se desea. El contenido de cada aplicación de LightSwitch consta de un conjunto de partes conocidas. En la tabla siguiente se muestran las partes definidas de una aplicación de LightSwitch.

Parte

Modelo de vista

Descripción

Navegación

NavigationViewModel

Proporciona acceso al panel Navegación utilizado para abrir pantallas.

Comandos

CommandsViewModel

Proporciona acceso al panel Barra de comandos utilizado para mostrar botones u otros comandos.

Pantallas activas

ActiveScreensViewModel

Proporciona acceso al panel Pantalla utilizado para mostrar pantallas.

Usuario actual

CurrentUserViewModel

Permite mostrar información sobre el usuario actual que inició la sesión.

Logo

LogoViewModel

Permite la presentación de una imagen especificada en la propiedad Logo.

Validación de pantalla

ValidationViewModel

Proporciona acceso a la interfaz de usuario de validación.

Para cada una de estas partes, LightSwitch proporciona un modelo de vista en el que los controles se pueden enlazar. LightSwitch proporciona un mecanismo que facilita el enlace a estos modelos de vista: ComponentViewModelService. Cuando se especifica como un atributo para un control, este servicio usa MEF para encontrar el modelo de vista especificado, crear una instancia en él y configurarlo para que sea el contexto de datos para el control. En el ejemplo de código siguiente se muestra cómo tiene un cuadro de lista su contexto de datos establecido para ser el modelo de vista de comandos.

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

Para definir el shell

  1. En el Explorador de soluciones, en el proyecto ShellExtension.Client, elija la carpeta Presentación, Shells y después abra el archivo ShellSample.xaml.

  2. Reemplace el contenido por lo siguiente.

    <UserControl x:Class="ShellExtension.Presentation.Shells.ShellSample"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://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>
    

    Esto contiene la lista de código completa del archivo ShellSample.xaml; las diferentes secciones se explican en pasos posteriores. Puede omitir cualquier error referentes a tipos que faltan; también se agregarán más adelante.

  3. En el Explorador de soluciones, abra el menú contextual para el nodo Presentación, Shells en el proyecto ShellExtension.Client y elija Agregar nuevo elemento.

  4. En el cuadro de diálogo Agregar nuevo elemento, expanda el nodo Silverlight y, a continuación, elija Diccionario de recursos de Silverlight.

  5. En el campo Nombre, escriba TextBlockStyle y elija el botón Agregar.

  6. Reemplace el XAML existente por lo siguiente para definir un ResourceDictionary.

    <ResourceDictionary
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://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>
    

    El UserControl de XAML hace referencia al diccionario de recursos y especifica el estilo que se aplica a todos los controles TextBlock.

El XAML para el shell hace referencia a varios convertidores de valores; se definirá después.

Para definir convertidores de valores

  1. En el Explorador de soluciones, en el proyecto ShellExtension.Client, abra el menú contextual para el nodo Presentación, Shells y elija Agregar nuevo elemento.

  2. En el cuadro de diálogo Agregar nuevo elemento, expanda el nodo Código y elija Clase.

  3. En el campo Nombre, escriba Converters y elija el botón Agregar.

  4. Reemplace el contenido por el código siguiente.

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

Implementar el shell

Cuando se escribe un shell, se deben implementar muchos fragmentos de funcionalidad y se tendrán que utilizar varios sistemas en el tiempo de ejecución de LightSwitch. El primer paso consiste en actualizar la implementación predeterminada del shell en archivo de código subyacente.

Hh290138.collapse_all(es-es,VS.140).gifActualizar la implementación predeterminada de shell

La plantilla de shell proporciona un punto de partida para crear la extensión de shell. Se expandirá sobre la implementación básica para definir la funcionalidad que el shell proporcionará.

Para actualizar la implementación de shell

  1. En el Explorador de soluciones, en el proyecto ShellExtension.Client, elija la carpeta Presentación, Shells y después abra el archivo ShellSample.xaml.vb o ShellSample.xaml.cs.

  2. Reemplace las instrucciones Imports o using por lo siguiente.

    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
    
    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. Reemplace el código interior del espacio de nombres Presentation.Shells (Visual Basic) o ShellExtension.Presentation.Shells (C#) por lo siguiente.

    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 proporciona un objeto que implementa la interfaz IServiceProxy, que proporciona acceso a los servicios necesarios de LightSwitch. El segmento de código siguiente muestra cómo el objeto IServiceProxy se puede recuperar de MEF por medio del estático VsExportProviderService.

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

En el constructor del control principal, tendrá que suscribirse a algunas notificaciones que proporcionan algunos puntos importantes de enlace en el flujo de trabajo en el tiempo de ejecución de LightSwitch. Estas notificaciones son ScreenOpenedNotification, ScreenClosedNotification y ScreenReloadedNofitication. Además, si desea que el shell lea y escriba valores como lo hace el shell de ejemplo, debe también registrarse para el evento Closing en el objeto UserSettingsService. El segmento de código siguiente implementa el constructor y enlaza las notificaciones.

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 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(es-es,VS.140).gifPara mostrar las pantallas disponibles

El shell de ejemplo utiliza un control estándar TreeView de Silverlight para mostrar las pantallas en sus agrupaciones adecuadas. El siguiente extracto del archivo ShellSample.xaml implementa el panel Navegación.

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

El XAML especifica que el contexto de datos para el control TreeView se establece en el modelo de vista identificado por el valor Default.NavigationViewModel especificándolo como el valor ComponentViewModelService. La interfaz INavigationViewModel tiene una propiedad denominada NavigationItems que devuelve una colección observable de objetos INavigationItem. La propiedad ItemsSource del control TreeView está enlazada a esta colección.

Cada INavigationItem puede ser INavigationScreen, que representa una pantalla que se puede ejecutar, o INavigationGroup, que representa un contenedor de otros objetos INavigationItem. Por consiguiente, la plantilla de elemento para el control de vista de árbol se enlaza a la propiedad Children y cada elemento secundario es un sencillo control TextBlock cuya propiedad Text se establece en la propiedad DisplayName del objeto INavigationItem. Es importante observar que las pantallas que no puede ejecutar el shell, por ejemplo, pantallas parametrizadas o pantallas a las que el usuario no tiene acceso, se filtrarán por NavigationViewModel.

Hh290138.collapse_all(es-es,VS.140).gifPara abrir una pantalla

Cuando el usuario hace doble clic en un elemento de navegación de pantalla en el panel Navegación del shell del ejemplo, la pantalla se abrirá en una pestaña de un control de pestaña. El XAML del control TextBlock que se utiliza para mostrar cada elemento de navegación, especifica un controlador para el evento MouseLeftButtonDown y es en este controlador donde reside la lógica para la apertura de una pantalla. El segmento de código siguiente de la clase ShellSample agrega el controlador.

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

El contexto de datos del control TextBlock es una instancia de la interfaz INavigationItem. Si este objeto se puede convertir correctamente en una instancia de la interfaz INavigationScreen, la pantalla se debe mostrar (ejecutar). La propiedad ExecutableObject en el objeto INavigationScreen contiene un método denominado ExecuteAsync que producirá el runtime de LightSwitch para cargar la pantalla. Cuando la pantalla se carga, la notificación ScreenOpenedNotification se publicará y se llamará al controlador asociado.

El objeto ScreenOpenedNotification proporciona acceso a las instancias de las interfaces IScreenObject y IScreenView. IScreenView es la manera de obtener el control de la interfaz de usuario raíz de la pantalla. El shell de ejemplo toma este control y lo establece para que sea el contenido del elemento de pestaña. Muchas de las API en tiempo de ejecución usan IScreenObject de alguna manera, y tendría sentido que fuera el contexto de datos del control el que posee la interfaz de usuario de pantalla, en este ejemplo el elemento de pestaña. Sin embargo, IScreenObject falta en varias partes principales de funcionalidad, de modo que este ejemplo incluye una clase auxiliar denominada MyScreenObject que proporciona esta funcionalidad y, por consiguiente, se utiliza como el contexto de datos para el elemento de pestaña.

Para crear la clase MyScreenObject

  1. En el Explorador de soluciones, abra el menú contextual de la carpeta Presentación, Shells y elija Agregar, Clase.

  2. En el campo Nombre, escriba MyScreenObject y después haga clic en Agregar.

  3. Reemplace el contenido de la clase por lo siguiente:

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

Una vez abiertas las pantallas, es responsabilidad del shell realizar un seguimiento de qué pantalla es actual. Para realizar el seguimiento de la pantalla actual, establezca la propiedad Current en ActiveScreensViewModel. Para realizar el seguimiento de una pantalla abierta, esta propiedad se establece en la instancia IScreenObject que se acaba de abrir. El segmento de código siguiente de la clase ShellSample realiza el seguimiento de la pantalla abierta.

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 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(es-es,VS.140).gifControlar interacciones de pantalla

Una vez abiertas las pantallas, también se deben controlar las interacciones del usuario como cerrar o actualizar, o cambiar entre pantallas activas. Las notificaciones para el cierre de la pantalla y la actualización de la pantalla contienen IScreenObject que se cierra o se actualiza. Puesto que el contexto de datos del control que está hospedando la pantalla es en realidad el contenedor IScreenObject alrededor de MyScreenObject, debe obtener la instancia del objeto de la pantalla subyacente a partir de la instancia MyScreenObject, para poder compararla al objeto de la pantalla que es una parte de los argumentos de notificación. El código siguiente recupera la instancia IScreenObject subyacente llamando a la propiedad RealScreenObject en MyScreenObject.

Dim realScreenObject As IScreenObject = DirectCast(ti.DataContext, MyScreenObject).RealScreenObject
IScreenObject   realScreenObject = ((MyScreenObject)ti.DataContext).RealScreenObject;

El flujo de trabajo básico al cerrar una pantalla implica quitar el control que muestra la pantalla que se ha cerrado y, a continuación, establecer la pantalla actualmente activa para que sea una de las otras pantallas que aún están abiertas, si hay alguna abierta. En el ejemplo, quitar el control consiste en encontrar el elemento de pestaña adecuado y quitarlo de su elemento primario de control de pestaña. Una vez que se ha quitado, se debe establecer la propiedad Current en ActiveScreensViewModel para que sea la instancia subyacente IScreenObject, encapsulada por la instancia MyScreenObject, sirviendo como el contexto de datos para el elemento de pestaña recientemente seleccionado. El segmento de código siguiente de la clase ShellSample controla el cierre de la pantalla.

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

Cuando se actualiza una pantalla, realmente se cierra y se vuelve a ejecutar, lo que significa que IScreenObject para esa pantalla es un nuevo objeto. Los argumentos en la notificación ScreenReloadedNotification contienen la instancia IScreenObject para la instancia anterior de la pantalla. Esto permite buscar el control que hospedaba la pantalla original. Cuando se encuentra este control, el contexto de datos debe establecerse en un nuevo IScreenObject para la pantalla nueva. Para el ejemplo, esto significa que se tiene que crear un nuevo MyScreenObject que se ajuste al nuevo IScreenObject y después se coloque en el contexto de datos para el control de elemento de pestaña. El segmento de código siguiente de la clase ShellSample controla la actualización de la pantalla.

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
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(es-es,VS.140).gifImplementar comandos

El shell de ejemplo muestra los comandos disponibles en una barra de comandos en la parte superior del shell, que es simplemente un control Cuadro de lista estándar de Silverlight. El siguiente extracto del archivo ShellSample.xaml muestra la implementación.

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

En el XAML, observe el enlace de datos a CommandsViewModel y sus diversas propiedades por los controles especificados en la plantilla. Cada comando se muestra en ListBox mediante Button cuyo contenido es un control TextBlock y Image. La propiedad IsEnabled del botón está enlazada a la propiedad IsEnabled de IShellCommand, y la propiedad Text del bloque de texto esté establecida en la propiedad DisplayName de IShellCommand.

En el controlador del evento Click para Button, ejecute el comando mediante la recuperación de la propiedad ExecuteableObject del objeto IShellCommand y después llame al método ExecuteAsync en él. El segmento de código siguiente de la clase ShellSample controla el cierre de la pantalla.

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 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(es-es,VS.140).gifControlar cambios de datos y validación

En LightSwitch, cada pantalla tiene su propia área de trabajo de datos. Si cualquiera de los datos de esa área de trabajo ha cambiado, se considerará que la pantalla se encuentra en estado modificado. El objeto IScreenObject no proporciona una propiedad simple que indica si se encuentra en estado modificado o no. Por consiguiente, esta información se tiene que obtener de los servicios de datos que proporcionan datos en el área de trabajo de la pantalla. Para ello, hay que registrarse para los eventos PropertyChanged en cada servicio de datos. Los extractos de código siguientes son de la clase MyScreenObject, que en este ejemplo se proporcionan como un contenedor de las instancias IScreenObject para proporcionar esta funcionalidad al usuario.

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

El controlador OnDataServicePropertyChanged comprueba la propiedad HasChanges en el objeto de detalles para el servicio de datos, como se muestra a continuación.

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

            this.IsDirty = dataService.Details.HasChanges;
        }

Para mostrar los errores de validación de una pantalla, primero se debe determinar si la pantalla tiene algún error. Determinar esto es similar a determinar si la pantalla está modificada porque IScreenObject no proporciona una propiedad simple que expone este estado. El objeto de detalles para la pantalla contiene los resultados de la validación y cuando ese conjunto de resultados cambia se desencadena una notificación PropertyChanged. Por consiguiente, solo tiene que registrarse para esta notificación. Los extractos de código siguientes son de la clase MyScreenObject.

' Register for property changed events on the details object.
            AddHandler CType(screenObject.Details, INotifyPropertyChanged).PropertyChanged, AddressOf Me.OnDetailsPropertyChanged
// Register for property changed events on the details object.
        ((INotifyPropertyChanged)screenObject.Details).PropertyChanged += this.OnDetailsPropertyChanged;

En el controlador de esta notificación hay que comprobar si la propiedad que se ha cambiado en el objeto de detalles es la propiedad ValidationResults. Si es así, se sabrá que los resultados de la validación han cambiado. En el shell de ejemplo, uno de los bloques de texto del elemento de pestaña tiene su propiedad Visibility enlazada a la propiedad ValidationResults en la instancia MyScreenObject. En el controlador para la notificación PropertyChanged, la notificación PropertyChanged se publica para la propiedad en MyScreenObject, de modo que el texto que indica que hay errores de validación se muestra en el elemento de pestaña.

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 void OnDetailsPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if ( String.Equals(e.PropertyName, "ValidationResults", StringComparison.OrdinalIgnoreCase) )
            {
                if ( null != this.PropertyChanged )
                    PropertyChanged(this, new PropertyChangedEventArgs("ValidationResults"));
            }
        }

En el ejemplo, al mantener el mouse sobre este texto, que es simplemente un signo de admiración, dará lugar a una información sobre herramientas que muestra todos los errores de esa pantalla. Esto se define en el siguiente fragmento del archivo 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(es-es,VS.140).gifMostrar el usuario actual

Si la autenticación está habilitada en una aplicación de LightSwitch, sería útil para los usuarios finales si el shell se usara para mostrar el nombre del usuario que está utilizando actualmente la aplicación. El shell de ejemplo muestra esta información en la esquina inferior izquierda. Si la autenticación no está habilitada, se muestra el valor “La autenticación no está habilitada”. A continuación se muestra el fragmento del archivo ShellSample.xaml que define los controles que muestran esta información.

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

La información sobre el usuario actual está disponible en el modelo de la vista CurrentUserViewModel. El bloque de texto que muestra el nombre de usuario actual enlaza a la propiedad CurrentUserDisplayName en ese modelo de vista.

Proporcionar un nombre descriptivo y una descripción

El nombre y la descripción del shell se definen en el archivo ShellSample.lsml; los valores predeterminados son “ShellSample” y “ShellSample description”. Estos se exponen al usuario del shell en el Diseñador de aplicaciones. Por consiguiente, deseará cambiar ambos valores por algo más significativo.

Para actualizar el nombre y la descripción

  1. En el Explorador de soluciones, elija el proyecto ShellExtension.Common.

  2. Expanda los nodos de Metadatos y Shells, y abra el archivo ShellSample.lsml.

  3. En el elemento Shell.Attributes, reemplace los valores DisplayName y Description, como se muestra a continuación.

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

    Nota

    También puede almacenar estos valores como recursos en el archivo ModuleResources.resx.Para obtener más información, vea “Para actualizar los metadatos de control” en Tutorial: Crear una extensión de control de detalle.

Probar la extensión de shell

Puede probar la extensión de tipo empresarial en una instancia experimental de Visual Studio. Si todavía no ha probado ningún proyecto de extensibilidad LightSwitch, primero tiene que habilitar la instancia experimental.

Para habilitar una instancia experimental

  1. En el Explorador de soluciones, elija el proyecto BusinessTypeExtension.Vsix.

  2. En la barra de menús, elija Proyecto, Propiedades de BusinessTypeExtension.Vsix.

  3. En la pestaña Depurar, en Acción de inicio, elija Programa externo de inicio.

  4. Escriba la ruta del archivo ejecutable de Visual Studio, devenv.exe.

    De forma predeterminada en un sistema de 32 bits la ruta de acceso es C:\Archivos de programa\Microsoft Visual Studio 12.0\Common7\IDE\devenv.exe; en un sistema de 64 bits, es C:\Archivos de programa (x86)\Microsoft Visual Studio 12.0\Common7\IDE\devenv.exe.

  5. En el campo Argumentos de la línea de comandos, escriba /rootsuffix Exp.

    Nota

    De forma predeterminada, todos los proyectos de extensibilidad de LightSwitch subsiguientes también usarán este valor.

Para probar el shell

  1. En la barra de menús, elija Depurar, Iniciar depuración. Se abre una instancia experimental de Visual Studio.

  2. En la instancia experimental, en la barra de menús, elija Archivo, Nuevo, Proyecto.

  3. En el cuadro de diálogo Abrir proyecto, seleccione un proyecto existente de la aplicación de LightSwitch y después elija el botón Aceptar.

  4. En la barra de menús, elija Proyecto, ProjectName Propiedades.

  5. En el Diseñador de proyectos, en la pestaña Extensiones, active la casilla ShellExtension.

  6. Seleccione la pestaña Propiedades generales y en la lista Shell elija My Sample Shell.

  7. En la barra de menús, elija Depurar, Iniciar depuración.

    Observe que no hay pantalla predeterminada y que tiene que hacer doble clic en un elemento del panel Navegación para abrir una pantalla.

Pasos siguientes

Con esto concluye el tutorial sobre la extensión de shell; ahora debería tener una extensión de shell totalmente operativa que se puede volver a usar en cualquier proyecto de LightSwitch. Esto ha sido simplemente un ejemplo sobre una extensión de shell; puede que desee crear un shell muy diferente en cuanto a comportamiento o diseño. Los mismos pasos y principios básicos se aplican a cualquier extensión de shell, pero hay otros conceptos que se aplican en otras situaciones.

Si va a distribuir la extensión, debe realizar algunas acciones más. Para asegurarse de que la información que se muestra para la extensión en el Diseñador de proyectos y en el Administrador de extensiones es correcta, debe actualizar las propiedades del paquete VSIX. Para obtener más información, vea Cómo: Establecer propiedades de paquete VSIX. Además, si va a distribuir la extensión públicamente, hay varios aspectos que se deben considerar. Para obtener más información, vea Cómo: Distribuir una extensión de LightSwitch.

Vea también

Tareas

Cómo: Distribuir una extensión de LightSwitch

Cómo: Establecer propiedades de paquete VSIX

Conceptos

LightSwitch Extensibility Toolkit para Visual Studio 2013