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
En la barra de menús de Visual Studio, elija Archivo, Nuevo, Proyecto.
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.
En el campo Nombre, escriba ShellExtension como nombre de la biblioteca de extensión.
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
En el Explorador de soluciones, elija el proyecto ShellExtension.Lspkg.
En la barra de menús, elija Proyecto, Agregar nuevo elemento.
En el cuadro de diálogo Agregar nuevo elemento, elija Shell.
En campo Nombre, escriba ShellSample como nombre de la extensión.
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
En el Explorador de soluciones, abra el menú contextual del proyecto ShellExtension.Client y luego elija Agregar referencia.
En el cuadro de diálogo Agregar referencia, agregue una referencia a System.Windows.Controls.dll.
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
En el Explorador de soluciones, en el proyecto ShellExtension.Client, elija la carpeta Presentación, Shells y después abra el archivo ShellSample.xaml.
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.
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.
En el cuadro de diálogo Agregar nuevo elemento, expanda el nodo Silverlight y, a continuación, elija Diccionario de recursos de Silverlight.
En el campo Nombre, escriba TextBlockStyle y elija el botón Agregar.
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
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.
En el cuadro de diálogo Agregar nuevo elemento, expanda el nodo Código y elija Clase.
En el campo Nombre, escriba Converters y elija el botón Agregar.
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.
Actualizar 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
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.
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;
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);
}
Para 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.
Para 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
En el Explorador de soluciones, abra el menú contextual de la carpeta Presentación, Shells y elija Agregar, Clase.
En el campo Nombre, escriba MyScreenObject y después haga clic en Agregar.
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;
}
Controlar 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;
}
}
}
Implementar 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();
}
Controlar 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>
Mostrar 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
En el Explorador de soluciones, elija el proyecto ShellExtension.Common.
Expanda los nodos de Metadatos y Shells, y abra el archivo ShellSample.lsml.
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
En el Explorador de soluciones, elija el proyecto BusinessTypeExtension.Vsix.
En la barra de menús, elija Proyecto, Propiedades de BusinessTypeExtension.Vsix.
En la pestaña Depurar, en Acción de inicio, elija Programa externo de inicio.
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.
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
En la barra de menús, elija Depurar, Iniciar depuración. Se abre una instancia experimental de Visual Studio.
En la instancia experimental, en la barra de menús, elija Archivo, Nuevo, Proyecto.
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.
En la barra de menús, elija Proyecto, ProjectName Propiedades.
En el Diseñador de proyectos, en la pestaña Extensiones, active la casilla ShellExtension.
Seleccione la pestaña Propiedades generales y en la lista Shell elija My Sample Shell.
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