Exemplarische Vorgehensweise: Erstellen einer Shell-Erweiterung
Diese exemplarische Vorgehensweise veranschaulicht, wie eine Shellerweiterung für LightSwitch erstellt wird. Die Shell für eine LightSwitch-Anwendung ermöglicht es Benutzern, mit der Anwendung zu interagieren. Sie manifestiert die navigierbaren Elemente, ausgeführten Bildschirme, zugeordneten Befehle, aktuellen Benutzerinformationen und andere nützliche Informationen, die Bestandteil der Shelldomäne sind. LightSwitch bietet eine direkte und leistungsfähige Shell. Sie können jedoch auch eine eigene Shell erstellen, die Ihre eigenen kreativen Möglichkeiten zur Interaktion mit den verschiedenen Segmenten der LightSwitch-Anwendung bereitstellt.
In dieser exemplarischen Vorgehensweise erstellen Sie eine Shell, die der Standardshell ähnelt, aber einige feine Unterschiede in Darstellung und Verhalten aufweist. Die Befehlsleiste eliminiert die Befehlsgruppen und verschiebt die Schaltfläche Bildschirm entwerfen nach links. Das Navigationsmenü ist fixiert. Es wird kein Startbildschirm angezeigt, und die Bildschirme werden durch Doppelklick auf die Menüelemente geöffnet. Die Bildschirme implementieren einen unterschiedlichen Validierungsindikator, und aktuelle Benutzerinformationen werden immer in der unteren linken Ecke der Shell angezeigt. Anhand dieser Unterschiede lassen sich mehrere nützliche Techniken für die Erstellung von Shellerweiterungen veranschaulichen.
Die Struktur einer Shellerweiterung umfasst drei Hauptkomponenten:
Das Managed Extensibility Framework (MEF), das die Implementierung des Shellvertrags exportiert.
Die Extensible Application Markup Language (XAML), die die Steuerelemente beschreibt, die für die Benutzeroberfläche der Shell verwendet werden.
Der Visual Basic- oder C#-Code hinter der XAML, der das Verhalten der Steuerelemente implementiert und mit der LightSwitch-Laufzeit interagiert.
Das Erstellen einer Shellerweiterung umfasst die folgenden Aufgaben:
Erstellen eines Shellerweiterungsprojekts
Hinzufügen von Verweisen zu Namespaces
Erstellen des MEF-Segments
Definieren der Shell
Implementieren der Shell
Angeben eines Anzeigenamens und einer Beschreibung
Testen der Shellerweiterung
Vorbereitungsmaßnahmen
Visual Studio 2013 Professional
Visual Studio 2013 SDK
LightSwitch Extensibility Toolkit für Visual Studio 2013
Erstellen eines Shellerweiterungsprojekts
Der erste Schritt besteht darin, ein Projekt zu erstellen und eine LightSwitch-Shellvorlage hinzuzufügen.
So erstellen Sie ein Erweiterungsprojekt
Wählen Sie in der Visual Studio-Menüleiste Datei, Neu und Projekt aus.
Erweitern Sie im Dialogfeld Neues Projekt den Knoten Visual Basic oder Visual C#, erweitern Sie den Knoten LightSwitch, wählen Sie dann den Knoten Erweiterungen und anschließend die Vorlage LightSwitch-Erweiterungsbibliothek aus.
Geben Sie im Feld Name den Namen ShellExtension für die Erweiterungsbibliothek ein.
Wählen Sie die Schaltfläche OK, um eine Projektmappe zu erstellen, die die sieben Projekte enthält, die für die Erweiterung erforderlich sind.
So wählen Sie einen Erweiterungstyp aus
Wählen Sie im Projektmappen-Explorer das Projekt ShellExtension.Lspkg aus.
Wählen Sie in der Menüleiste Projekt, Neues Element hinzufügen aus.
Wählen Sie im Dialogfeld Neues Element hinzufügen den Eintrag Shell aus.
Geben Sie im Feld Name den Namen ShellSample für die Erweiterung ein.
Klicken Sie auf die Schaltfläche OK. Dateien werden für mehrere Projekte in der Projektmappe hinzugefügt.
Hinzufügen von Verweisen
Die Shellerweiterung muss auf einige Namespaces verweisen, die nicht Bestandteil der Standardvorlage sind.
So fügen Sie Verweise hinzu
Öffnen Sie im Projektmappen-Explorer das Kontextmenü für das ShellExtension.Client-Projekt, und wählen Sie Verweis hinzufügen aus.
Fügen Sie im Dialogfeld Verweis hinzufügen einen Verweis auf System.Windows.Controls.dll hinzu.
Fügen Sie im Dialogfeld Verweis hinzufügen einen Verweis auf Microsoft.LightSwitch.ExportProvider.dll hinzu.
Die Assembly befindet sich im Ordner PrivateAssembly unter dem Ordner für die Visual Studio-IDE.
Erstellen des MEF-Segments
Um die Implementierung einer Shell für das MEF (Managed Extensibility Framework) verfügbar zu machen, muss eine Klasse erstellt werden, mit der die IShell-Schnittstelle implementiert wird. Zudem sind die erforderlichen Attributausgestaltungen bereitzustellen. Es gibt zwei solche Attribute, die erforderlich sind: Export und Shell. Über das Attribut Export wird dem MEF mitgeteilt, welcher Vertrag von der Klasse implementiert wird. Das Attribut Shell hingegen enthält die Metadaten, mit denen die Implementierung Ihrer Shell von anderen Implementierungen unterschieden wird. Die Implementierung wird über die Projektvorlage hinzugefügt. Sie befindet sich im Ordner Präsentation, Shells, Komponenten im ShellExtension.Client-Projekt. Das folgende Beispiel zeigt die Implementierung.
<Export(GetType(IShell))>
<Shell(ShellSample.ShellId)>
Friend Class ShellSample
Implements IShell
[Export(typeof(IShell))]
[Shell(ShellSample.ShellId)]
internal class ShellSample : IShell
{
...
}
Die im Shell-Attribut angegebenen Daten fungieren als Bezeichner der Shell. Der Wert muss folgendem Format entsprechen: <Module Name>:<Shell Name>. Der Name des Moduls wird in der module.lsml-Datei angegeben, die das Modul beschreibt. Diese Datei und der Modulname werden durch die Projektvorlage generiert. Der Name der Shell wird in der ProjectName.lsml-Datei angegeben, die die Shell beschreibt. Die IShell-Schnittstelle besitzt zwei Methoden: eine gibt den Namen der Shell zurück, der dem im Shell-Attribut angegebenen Wert entspricht, die andere gibt einen URI (Uniform Resource Locator) an die XAML zurück, der eine eingebettete Ressource in der erstellten Assembly ist.
Definieren der Shell
Beim Entwickeln einer Shellerweiterung für LightSwitch haben Sie Freiheit, jedes Steuerelement zu erstellen und zu verwenden, mit dem Sie die gewünschte Darstellung erzielen können. Der Inhalt einer LightSwitch-Anwendung besteht aus einer Reihe bekannter Segmente. Die folgende Tabelle zeigt die definierten Segmente einer LightSwitch-Anwendung an.
Segment |
ViewModel-Element |
Beschreibung |
---|---|---|
Navigation |
NavigationViewModel |
Bietet Zugriff auf den Bereich Navigation, der zum Öffnen von Bildschirmen verwendet wird. |
Befehle |
CommandsViewModel |
Bietet Zugriff auf den Bereich Befehlsleiste, der zur Anzeige von Schaltflächen oder anderen Befehlen verwendet wird. |
Aktive Bildschirme |
ActiveScreensViewModel |
Bietet Zugriff auf den Bereich Bildschirm, der für die Anzeige von Bildschirmen verwendet wird. |
Aktueller Benutzer |
CurrentUserViewModel |
Ermöglicht das Anzeigen von Informationen über den aktuell angemeldeten Benutzer. |
Logo |
LogoViewModel |
Ermöglicht die Anzeige eines Bilds, das in der Logo-Eigenschaft angegeben ist. |
Bildschirmvalidierung |
ValidationViewModel |
Bietet Zugriff auf die Benutzeroberfläche für die Validierung. |
Für jedes dieser Segmente stellt LightSwitch ein ViewModel bereit, an das die Steuerelemente gebunden werden können. LightSwitch enthält eine Methode, die eine einfache Bindung an diese ViewModels ermöglicht: ComponentViewModelService. Ist dieser Dienst als Attribut für ein Steuerelement angegeben, sucht er mit dem MEF nach dem angegebenen ViewModel, instanziiert es und legt es als Datenkontext für das Steuerelement fest. Das folgende Codebeispiel zeigt, wie für ein Listenfeld der Datenkontext auf das ViewModel für Befehle festgelegt wird.
<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>
So definieren Sie die Shell
Wählen Sie im Projektmappen-Explorer den Ordner Präsentation, Shells des ShellExtension.Client-Projekts, und öffnen Sie dann die Datei ShellSample.xaml.
Ersetzen Sie den Inhalt durch Folgendes.
<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>
Dies enthält die vollständige Codeauflistung für die ShellSample.xaml-Datei, die verschiedenen Abschnitte werden in späteren Schritten erklärt. Sie können alle Fehler bezüglich fehlender Typen ignorieren, diese werden ebenfalls später hinzugefügt.
Öffnen Sie im Projektmappen-Explorer das Kontextmenü für den Knoten Präsentation, Shells im ShellExtension.Client-Projekt, und wählen Sie Neues Element hinzufügen aus.
Erweitern Sie im Dialogfeld Neues Element hinzufügen den Knoten Silverlight, und wählen Sie Silverlight-Ressourcenwörterbuch aus.
Geben Sie im Textfeld Name die Zeichenfolge TextBlockStyle ein, und klicken Sie dann auf die Schaltfläche Hinzufügen.
Ersetzen Sie den vorhandenen XAML-Code durch folgenden Code, um ResourceDictionary zu definieren.
<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>
Auf das Ressourcenwörterbuch wird durch den UserControl-XAML-Code verwiesen, und es gibt den Stil an, der für alle TextBlock-Steuerelemente übernommen wird.
Die XAML für die Shell verweist auf einige Wertkonverter, die Sie nachfolgend definieren.
So definieren Sie Wertkonverter
Öffnen Sie im Projektmappen-Explorer das Kontextmenü für den Knoten Präsentation, Shells im ShellExtension.Client-Projekt, und wählen Sie Neues Element hinzufügen aus.
Erweitern Sie im Dialogfeld Neues Element hinzufügen den Knoten Code, und wählen Sie Klasse aus.
Geben Sie im Textfeld Name die Zeichenfolge Converters ein, und klicken Sie dann auf die Schaltfläche Hinzufügen.
Ersetzen Sie den Inhalt durch den folgenden Code.
Imports System Imports System.Collections.Generic Imports System.Globalization Imports System.Linq Imports System.Text Imports System.Windows Imports System.Windows.Data Imports System.Windows.Media Imports Microsoft.LightSwitch Imports Microsoft.LightSwitch.Details Imports Microsoft.LightSwitch.Client Imports Microsoft.LightSwitch.Details.Client Namespace Presentation.Shells Public Class WorkspaceDirtyConverter Implements IValueConverter Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert Return If(CType(value, Boolean), Visibility.Visible, Visibility.Collapsed) End Function Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack Throw New NotSupportedException() End Function End Class Public Class ScreenHasErrorsConverter Implements IValueConverter Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert Return If(CType(value, Boolean), Visibility.Visible, Visibility.Collapsed) End Function Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack Throw New NotSupportedException() End Function End Class Public Class ScreenResultsConverter Implements IValueConverter Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert Dim results As ValidationResults = value Dim sb As StringBuilder = New StringBuilder() For Each result As ValidationResult In results.Errors sb.Append(String.Format("Errors: {0}", result.Message)) Next Return sb.ToString() End Function Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack Throw New NotSupportedException() End Function End Class Public Class CurrentUserConverter Implements IValueConverter Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert Dim currentUser As String = value If currentUser Is Nothing OrElse currentUser.Length = 0 Then Return "Authentication is not enabled." End If Return currentUser End Function Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack Throw New NotSupportedException() End Function End Class End Namespace
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(); } } }
Implementieren der Shell
Beim Schreiben einer Shell müssen zahlreiche Funktionen implementiert und mehrere Systeme in der LightSwitch-Laufzeit verwendet werden. Der erste Schritt besteht darin, die standardmäßige Implementierung der Shell in der Code-Behind-Datei zu aktualisieren.
Aktualisieren der standardmäßigen Shellimplementierung
Die Shellvorlage stellt einen Ausgangspunkt für das Erstellen der Shellerweiterung bereit. Die Basisimplementierung wird erweitert, um die Funktionalität zu definieren, die Ihre Shell bereitstellen soll.
So aktualisieren Sie die Shellimplementierung
Wählen Sie im Projektmappen-Explorer im ShellExtension.Client-Projekt den Ordner Präsentation, Shells aus, und öffnen Sie dann die ShellSample.xaml.vb-Datei bzw. die ShellSample.xaml.cs-Datei.
Ersetzen Sie die Imports-Anweisungen bzw. die using-Anweisungen durch Folgendes.
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;
Ersetzen Sie den Code im Presentation.Shells-Namespace (Visual Basic) bzw. im ShellExtension.Presentation.Shells-Namespace (C#) durch den folgenden Code.
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 enthält ein Objekt, das die IServiceProxy-Schnittstelle implementiert, die Zugriff auf die erforderlichen LightSwitch-Dienste bietet. Das folgende Codesegment zeigt, wie das IServiceProxy-Objekt vom MEF mithilfe des statischen VsExportProviderService-Elements abgerufen werden kann.
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;
}
}
Im Konstruktor für das Hauptsteuerelement sind mehrere Benachrichtigungen zu abonnieren, die einige wichtige Hookpunkte für den Workflow der LightSwitch-Laufzeit bereitstellen. Diese Benachrichtigungen sind ScreenOpenedNotification, ScreenClosedNotification und ScreenReloadedNofitication. Sofern Sie möchten, dass Ihre Shell ebenso wie die Beispielshell Einstellungen liest und schreibt, sollten Sie zudem die Registrierung des Closing-Ereignisses auf dem UserSettingsService-Objekt vornehmen. Das folgende Codesegment implementiert den Konstruktor und bindet die Benachrichtigungen ein.
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);
}
So zeigen Sie verfügbare Bildschirme an
Die Beispielshell verwendet ein standardmäßiges TreeView-Steuerelement von Silverlight, um die Bildschirme in ihren entsprechenden Gruppierungen anzuzeigen. Der folgende Auszug aus der ShellSample.xaml-Datei implementiert den Bereich Navigation.
<!-- 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>
Die XAML gibt an, dass der Datenkontext für das TreeView-Steuerelement auf das ViewModel festgelegt wird, das durch den Default.NavigationViewModel-Wert bezeichnet wird, indem dieser als Wert für ComponentViewModelService definiert ist. Die INavigationViewModel-Schnittstelle verfügt über eine Eigenschaft mit dem Namen NavigationItems, die eine Auflistung von INavigationItem-Objekten zurückgibt. Die ItemsSource-Eigenschaft des TreeView-Steuerelements ist an diese Auflistung gebunden.
Jedes INavigationItem-Element kann als INavigationScreen-Objekt fungieren, das einen ausführbaren Bildschirm darstellt. Es kann auch als INavigationGroup-Objekt fungieren, das einen Container für andere INavigationItem-Objekte darstellt. Daher ist die Elementvorlage für das Strukturansicht-Steuerelement an die Children-Eigenschaft gebunden, und jedes untergeordnete Element ist ein einfaches TextBlock-Steuerelement, dessen Text-Eigenschaft auf die DisplayName-Eigenschaft des INavigationItem-Objekts festgelegt ist. Beachten Sie, dass nicht von der Shell ausführbare Bildschirme (beispielsweise parametrisierte Bildschirme oder Bildschirme, auf die der Benutzer keinen Zugriff hat) von NavigationViewModel gefiltert werden.
So öffnen Sie einen Bildschirm
Durch einen Doppelklick des Benutzers auf ein Bildschirmnavigationselement im Bereich Navigation der Beispielshell wird ein Bildschirm auf einer Registerkarte in einem Registerkarten-Steuerelement geöffnet. Die XAML für das TextBlock-Steuerelement, mit dem die einzelnen Navigationselemente angezeigt werden, gibt einen Handler für das MouseLeftButtonDown-Ereignis an. In diesem Handler befindet sich die Logik zum Öffnen eines Bildschirms. Mit dem folgenden Codesegment aus der ShellSample-Klasse wird der Handler hinzugefügt.
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();
}
Der Datenkontext des TextBlock-Steuerelements ist eine Instanz der INavigationItem-Schnittstelle. Wenn dieses Objekt erfolgreich in eine Instanz der INavigationScreen-Schnittstelle umgewandelt werden kann, sollte der Bildschirm angezeigt (ausgeführt) werden. Die ExecutableObject-Eigenschaft auf dem INavigationScreen-Objekt enthält eine ExecuteAsync-Methode, die veranlasst, dass der Bildschirm von der LightSwitch-Laufzeit geladen wird. Ist der Bildschirm geladen, wird die Benachrichtigung ScreenOpenedNotification veröffentlicht und der entsprechende Handler aufgerufen.
Das ScreenOpenedNotification-Objekt bietet Zugriff auf Instanzen der Schnittstellen IScreenObject und IScreenView. Über die IScreenView-Schnittstelle wird das Stamm-UI-Steuerelement dieses Bildschirms abgerufen. Die Beispielshell legt dann dieses Steuerelement als Inhalt des Registerkartenelements fest. Viele der Laufzeit-APIs verwenden IScreenObject auf die eine oder andere Weise, daher ist es sinnvoll, dieses Objekt als Datenkontext für das Steuerelement festzulegen, das die Benutzeroberfläche besitzt (in diesem Beispiel das Registerkartenelement). Jedoch fehlen in IScreenObject mehrere zentrale Funktionen. In diesem Beispiel ist daher eine Hilfsklasse mit dem Namen MyScreenObject enthalten, die diese Funktionen bereitstellt und folglich als Datenkontext für das Registerkartenelement fungiert.
So erstellen Sie die MyScreenObject-Klasse
Öffnen Sie im Projektmappen-Explorer das Kontextmenü für den Ordner Präsentation, Shells, und wählen Sie dann Hinzufügen und Klasse aus.
Geben Sie im Feld Name die Zeichenfolge MyScreenObject ein, und klicken Sie dann auf Hinzufügen.
Ersetzen Sie den Inhalt der Klasse durch Folgendes.
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; } } } }
Sobald die Bildschirme geöffnet sind, muss die Shell nachverfolgen, welcher Bildschirm aktuell ist. Um den aktuellen Bildschirm nachverfolgen zu können, legen Sie die Current-Eigenschaft auf ActiveScreensViewModel fest. Um einen geöffneten Bildschirm zu verfolgen, wird diese Eigenschaft auf die Instanz von IScreenObject festgelegt, die gerade geöffnet wurde. Mit dem folgenden Codesegment der ShellSample-Klasse wird der geöffnete Bildschirm verfolgt.
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;
}
Behandeln der Bildschirminteraktion
Sobald Bildschirme geöffnet sind, müssen auch die Benutzerinteraktionen wie Schließen, Aktualisieren oder Wechseln zwischen aktiven Bildschirmen behandelt werden. Die Benachrichtigungen für das Schließen oder Aktualisieren eines Bildschirms enthalten das IScreenObject-Objekt, das geschlossen oder aktualisiert wird. Der Datenkontext für das den Bildschirm hostende Steuerelement ist tatsächlich der MyScreenObject-Wrapper um IScreenObject. Daher muss die zugrunde liegende Bildschirmobjektinstanz von der MyScreenObject-Instanz abgerufen werden, damit sie mit dem Bildschirmobjekt verglichen werden kann, das Teil der Benachrichtigungsargumente ist. Mit folgendem Code wird die zugrunde liegende IScreenObject-Instanz durch den Aufruf der RealScreenObject-Eigenschaft auf MyScreenObject aufgerufen.
Dim realScreenObject As IScreenObject = DirectCast(ti.DataContext, MyScreenObject).RealScreenObject
IScreenObject realScreenObject = ((MyScreenObject)ti.DataContext).RealScreenObject;
Der grundlegende Workflow beim Schließen eines Bildschirms umfasst das Entfernen des Steuerelements, mit dem der gerade geschlossene Bildschirm angezeigt wurde. Zudem wird der zurzeit geöffnete Bildschirm zu den anderen noch geöffneten (sofern vorhanden) hinzugefügt. Im Beispiel wird beim Entfernen des Steuerelements nach dem entsprechenden Registerkartenelement gesucht und dieses aus dem übergeordneten Registerkarten-Steuerelement entfernt. Nach dieser Entfernung müssen Sie die Current-Eigenschaft auf ActiveScreensViewModel so festlegen, dass diese die zugrunde liegende IScreenObject-Instanz ist, die von der MyScreenObject-Instanz umschlossen wird, die wiederum den Datenkontext für das neu ausgewählte Registerkartenelement stellt. Im folgenden Codesegment der ShellSample-Klasse wird das Schließen eines Bildschirms behandelt.
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);
});
}
}
Wird ein Bildschirm aktualisiert, wird er im Grunde beendet und erneut ausgeführt. Das heißt, IScreenObject für den Bildschirm ist ein neues Objekt. Die Argumente in der Benachrichtigung ScreenReloadedNotification enthalten die IScreenObject-Instanz für die vorherige Bildschirminstanz. Auf diese Weise können Sie das Steuerelement suchen, von dem der ursprüngliche Bildschirm gehostet wurde. Nachdem dieses Steuerelement ermittelt ist, muss dessen Datenkontext auf das neue IScreenObject-Element für den neuen Bildschirm festgelegt werden. In diesem Beispiel wird dafür ein neues MyScreenObject-Element erstellt, mit dem IScreenObject umschlossen wird. Das Element wird dann im Datenkontext für das Registerkarten-Steuerelement platziert. Im folgenden Codesegment der ShellSample-Klasse wird das Aktualisieren eines Bildschirms behandelt.
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;
}
}
}
Implementieren von Befehlen
In der Beispielshell werden die verfügbaren Befehle auf einer Befehlsleiste oben in der Shell angezeigt. Dabei handelt es sich einfach um ein standardmäßiges ListBox-Element von Silverlight. Der folgende Auszug aus der ShellSample.xaml-Datei zeigt die Implementierung.
<!-- 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>
Beachten Sie im XAML-Code die Datenbindung an CommandsViewModel und dessen verschiedene Eigenschaften, die über die in der Vorlage angegebenen Steuerelemente erfolgt. Jeder Befehl in ListBox wird mit Button angezeigt, dessen Inhalt ein Image-Steuerelement und TextBlock ist. Die IsEnabled-Eigenschaft der Schaltfläche ist an die IsEnabled-Eigenschaft von IShellCommand gebunden, und die Text-Eigenschaft des Textblocks ist auf die DisplayName-Eigenschaft von IShellCommand festgelegt.
Im Handler des Click-Ereignisses für Button führen Sie den Befehl aus, indem Sie die ExecuteableObject-Eigenschaft auf dem IShellCommand-Objekt abrufen und dann die ExecuteAsync-Methode dafür aufrufen. Im folgenden Codesegment der ShellSample-Klasse wird das Schließen eines Bildschirms behandelt.
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();
}
Behandeln von Datenänderungen und Datenvalidierung
In LightSwitch verfügt jeder Bildschirm über einen eigenen Datenarbeitsbereich. Im Falle, dass Daten in diesem Arbeitsbereich geändert werden, gilt für den Bildschirm der Zustand Geändert. IScreenObject stellt keine einfache Eigenschaft bereit, die Ihnen sagt, ob eine Änderung vorliegt oder nicht. Daher müssen Sie diese Informationen aus den Datendiensten abrufen, mit denen Daten für den Arbeitsbereich des Bildschirms bereitgestellt werden. Dies erreichen Sie, indem Sie eine Registrierung für die PropertyChanged-Ereignisse auf jedem Datendienst vornehmen. Die folgenden Codeauszüge stammen aus der MyScreenObject-Klasse. Sie wird in diesem Beispiel als Wrapper um die IScreenObject-Instanzen verwendet, um diese Funktionalität für Sie bereitzustellen.
' 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));
Der OnDataServicePropertyChanged-Handler überprüft wie folgt die HasChanges-Eigenschaft des Detailobjekts für den Datendienst.
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;
}
Um die Validierungsfehler für einen Bildschirm anzuzeigen, müssen Sie zuerst feststellen, ob der Bildschirm einen Fehler aufweist. Diese Feststellung erfolgt nach denselben Schritten, mit denen Sie bestimmen, ob ein Bildschirm geändert wurde. Das heißt, IScreenObject stellt keine einfache Eigenschaft bereit, die diesen Zustand angibt. Das Detailobjekt für den Bildschirm enthält die Validierungsergebnisse. Wird dieses Resultset geändert, löst es eine PropertyChanged-Benachrichtigung aus. Daher ist nur eine Registrierung für diese Benachrichtigung erforderlich. Die folgenden Codeauszüge stammen aus der MyScreenObject-Klasse.
' 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;
Im Handler für diese Benachrichtigung überprüfen Sie, ob es sich bei der auf dem Detailobjekt geänderten Eigenschaft um die ValidationResults-Eigenschaft handelt. Ist das der Fall, wissen Sie, dass die Validierungsergebnisse geändert worden sind. In der Beispielshell ist die Visibility-Eigenschaft eines Textblocks im Registerkartenelement an die ValidationResults-Eigenschaft auf der MyScreenObject-Instanz gebunden. Im Handler für die PropertyChanged-Benachrichtigung wird die PropertyChanged-Benachrichtigung für die Eigenschaft auf MyScreenObject veröffentlicht. So wird der Text mit der Information, dass Validierungsfehler vorhanden sind, auf dem Registerkartenelement angezeigt.
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"));
}
}
Wenn Sie im Beispiel mit dem Mauszeiger auf diesen Text zeigen (bei dem es sich nur um ein Ausrufezeichen handelt), wird eine QuickInfo generiert, die alle Fehler für den Bildschirm anzeigt. Dies wird im folgenden Auszug aus der ShellSample.xaml-Datei definiert.
<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>
Anzeigen des aktuellen Benutzers
Bei aktivierter Authentifizierung in einer LightSwitch-Anwendung wäre es für die Benutzer hilfreich, wenn die Shell den Namen des Benutzers anzeigt, der derzeit die Anwendung nutzt. In der Beispielshell werden diese Informationen in der linken unteren Ecke angezeigt. Ist die Authentifizierung deaktiviert, wird der Wert "Die Authentifizierung ist nicht aktiviert" angezeigt. Im folgenden Auszug aus der ShellSample.xaml-Datei werden die Steuerelemente zur Anzeige dieser Informationen definiert.
<!-- 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>
Die Angabe des aktuellen Benutzers wird über das CurrentUserViewModel-ViewModel bereitgestellt. Der Textblock, in dem der Name des aktuellen Benutzers angegeben wird, ist an die CurrentUserDisplayName-Eigenschaft von diesem ViewModel gebunden.
Angeben eines Anzeigenamens und einer Beschreibung
Der Name und die Beschreibung für die Shell werden in der ShellSample.lsml-Datei definiert. Die Standardwerte lauten "ShellSample" und "ShellSample description". Diese werden dem Benutzer der Shell im Anwendungs-Designer verfügbar gemacht. Daher ändern Sie dies in aussagekräftigere Bezeichnungen ab.
So aktualisieren Sie den Namen und die Beschreibung
Wählen Sie im Projektmappen-Explorer das Projekt ShellExtension.Common aus.
Erweitern Sie die Knoten Metadaten und Shells, und öffnen Sie die Datei ShellSample.lsml.
Ersetzen Sie wie folgt im Shell.Attributes-Element die Werte DisplayName und Description.
<Shell.Attributes> <DisplayName Value="My Sample Shell"/> <Description Value="This is my first example of a shell extension."/> </Shell.Attributes>
Hinweis
Sie können diese Werte auch als Ressourcen in der ModuleResources.resx-Datei speichern.Weitere Informationen finden Sie unter "So aktualisieren Sie Steuerelement-Metadaten" in Exemplarische Vorgehensweise: Erstellen einer Detailsteuerelementerweiterung.
Testen der Shellerweiterung
Sie können die Geschäftstyperweiterung in einer experimentellen Instanz von Visual Studio testen. Falls Sie nicht bereits ein anderes LightSwitch-Erweiterungsprojekt getestet haben, müssen Sie die experimentelle Instanz zunächst aktivieren.
So aktivieren Sie eine experimentelle Instanz
Wählen Sie im Projektmappen-Explorer das BusinessTypeExtension.Vsix-Projekt aus.
Wählen Sie in der Menüleiste Projekt und dann die Option für Eigenschaften von BusinessTypeExtension.Vsix aus.
Wählen Sie auf der Registerkarte Debuggen unter Startaktion die Option Externes Programm starten aus.
Geben Sie den Pfad der ausführbaren Visual Studio-Datei (devenv.exe) ein.
Auf einem 32-Bit-System ist der Standardpfad C:\Programme\Microsoft Visual Studio 12.0\Common7\IDE\devenv.exe, und auf einem 64-Bit-System ist der Pfad C:\Programme (x86)\Microsoft Visual Studio 12.0\Common7\IDE\devenv.exe.
Geben Sie im Feld Befehlszeilenargumente die Zeichenfolge /rootsuffix Exp ein.
Hinweis
Alle folgenden LightSwitch-Erweiterungsprojekte verwenden standardmäßig ebenfalls diese Einstellung.
So testen Sie die Shell
Klicken Sie in der Menüleiste auf Debuggen und dann auf Debuggen starten. Eine experimentelle Instanz von Visual Studio wird geöffnet.
Wählen Sie in der experimentellen Instanz in der Menüleiste Datei, Neu und Projekt aus.
Wählen Sie im Dialogfeld Projekt öffnen ein beliebiges vorhandenes LightSwitch-Anwendungsprojekt und dann die Schaltfläche OK aus.
Wählen Sie in der Menüleiste Projekt und die Option für ProjectName-Eigenschaften aus.
Aktivieren Sie im Projekt-Designer auf der Registerkarte Erweiterungen das Kontrollkästchen ShellExtension.
Wählen Sie auf der Registerkarte Allgemeine Eigenschaften in der Liste Shell die Option Meine Beispielshell aus.
Klicken Sie in der Menüleiste auf Debuggen und dann auf Debuggen starten.
Beachten Sie, dass es keinen Standardbildschirm gibt. Um einen Bildschirm zu öffnen, müssen Sie auf ein Element im Bereich Navigation doppelklicken.
Nächste Schritte
Damit ist die exemplarische Vorgehensweise für die Shellerweiterung beendet. Sie sollten nun über eine voll funktionsfähige Shellerweiterung verfügen, die Sie in jedem LightSwitch-Projekt einsetzen können. Das war nur ein Beispiel für eine Shellerweiterung, möglicherweise möchten Sie eine Shell erstellen, die sich in Bezug auf Verhalten oder Layout erheblich unterscheidet. Die gleichen Schritte und Grundsätze gelten auch für jede andere Shellerweiterung, jedoch gibt es weitere Konzepte, die in anderen Situationen Anwendung finden.
Wenn Sie die Erweiterung verteilen möchten, sind einige zusätzliche Schritte erforderlich. Um die Richtigkeit der Informationen sicherzustellen, die für die Erweiterung im Projekt-Designer und Erweiterungs-Manager angezeigt werden, müssen die Eigenschaften für das VSIX-Paket aktualisiert werden. Weitere Informationen finden Sie unter Gewusst wie: VSIX-Paketeigenschaften. Außerdem sind bei einer geplanten öffentlichen Verteilung der Erweiterung einige Punkte zu berücksichtigen. Weitere Informationen finden Sie unter Gewusst wie: Verteilen einer LightSwitch-Erweiterung.
Siehe auch
Aufgaben
Gewusst wie: Verteilen einer LightSwitch-Erweiterung
Gewusst wie: VSIX-Paketeigenschaften