Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Een van de belangrijkste doelen van het VisualStudio.Extensibility-model is om extensies buiten het Visual Studio-proces uit te voeren. Deze beslissing introduceert een obstakel voor het toevoegen van UI-ondersteuning aan extensies, omdat de meeste UI-frameworks in-process zijn.
Remote UI is een reeks klassen waarmee u WPF-besturingselementen (Windows Presentation Foundation) kunt definiëren in een out-of-process-extensie en deze kunt weergeven als onderdeel van de Visual Studio-gebruikersinterface.
Remote UI is sterk gericht op het ontwerppatroon Model-View-ViewModel, dat afhankelijk is van XAML (Extensible Application Markup Language) en data-binding, commando's (in plaats van gebeurtenissen) en triggers (in plaats van interactie met de logische structuur vanuit codeachter).
Hoewel Remote UI is ontwikkeld ter ondersteuning van out-of-process extensies, gebruiken VisualStudio.Extensibility API's die afhankelijk zijn van Remote UI, zoals ToolWindow, Remote UI ook voor in-process extensies.
De belangrijkste verschillen tussen de externe gebruikersinterface en de normale WPF-ontwikkeling zijn:
- De meeste externe UI-bewerkingen, waaronder binding met de gegevenscontext en de uitvoering van opdrachten, zijn asynchroon.
- Wanneer u gegevenstypen definieert die moeten worden gebruikt in externe gebruikersinterface-gegevenscontexten, moeten ze worden ingericht met de
DataContractkenmerken enDataMembermoet het type serialiseerbaar zijn door de externe gebruikersinterface (zie hier voor meer informatie). - Remote UI staat het verwijzen naar uw eigen aangepaste bedieningselementen niet toe.
- Een extern gebruikersbesturingselement is volledig gedefinieerd in één XAML-bestand dat verwijst naar één (maar mogelijk complex en geneste) gegevenscontextobject.
- Remote UI biedt geen ondersteuning voor code-behind of event handlers (workarounds worden beschreven in het document Geavanceerde Remote UI-concepten).
- Een extern gebruikersbeheer wordt geïnstantieerd in het Visual Studio-proces, niet het proces dat als host fungeert voor de extensie: de XAML kan niet verwijzen naar typen en assembly's uit de extensie, maar kan verwijzen naar typen en assembly's uit het Visual Studio-proces.
Een Externe UI Hello World-extensie maken
Begin met het maken van de meest eenvoudige remote UI-extensie. Volg de instructies in Het maken van uw eerste out-of-process Visual Studio-extensie.
U hebt nu een werkende extensie met één opdracht. De volgende stap is het toevoegen van een ToolWindow en een RemoteUserControl. Dit RemoteUserControl is het equivalent van een WPF-gebruikerscontrole in de Remote UI.
U eindigt met vier bestanden:
- een
.csbestand voor de opdracht waarmee het taakvenster wordt geopend, - een
.csbestand voor hetToolWindowdieRemoteUserControlaan Visual Studio levert, - een
.csbestand voor deRemoteUserControldat verwijst naar de eigen XAML-definitie, - een
.xamlbestand voor deRemoteUserControl.
Later voegt u een gegevenscontext toe voor het RemoteUserControl, wat het ViewModel in het MVVM-patroon (Model-View-ViewModel) vertegenwoordigt.
De opdracht bijwerken
Werk de code van de opdracht bij om het taakvenster weer te geven met behulp van ShowToolWindowAsync:
public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}
U kunt ook overwegen om te wijzigen CommandConfiguration en string-resources.json voor een geschikter weergavebericht en plaatsing:
public override CommandConfiguration CommandConfiguration => new("%MyToolWindowCommand.DisplayName%")
{
Placements = new[] { CommandPlacement.KnownPlacements.ViewOtherWindowsMenu },
};
{
"MyToolWindowCommand.DisplayName": "My Tool Window"
}
Het taakvenster maken
Maak een nieuw MyToolWindow.cs bestand en definieer een MyToolWindow klasse die wordt uitgebreid ToolWindow.
De GetContentAsync methode moet een IRemoteUserControl waarde retourneren die u in de volgende stap definieert. Aangezien de afstandsbediening wegwerpbaar is, moet u deze verwijderen door de Dispose(bool) methode te overschrijven.
namespace MyToolWindowExtension;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.ToolWindows;
using Microsoft.VisualStudio.RpcContracts.RemoteUI;
[VisualStudioContribution]
internal class MyToolWindow : ToolWindow
{
private readonly MyToolWindowContent content = new();
public MyToolWindow(VisualStudioExtensibility extensibility)
: base(extensibility)
{
Title = "My Tool Window";
}
public override ToolWindowConfiguration ToolWindowConfiguration => new()
{
Placement = ToolWindowPlacement.DocumentWell,
};
public override async Task<IRemoteUserControl> GetContentAsync(CancellationToken cancellationToken)
=> content;
public override Task InitializeAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
protected override void Dispose(bool disposing)
{
if (disposing)
content.Dispose();
base.Dispose(disposing);
}
}
De bediening voor de externe gebruiker creëren
Voer deze actie uit in drie bestanden:
Klasse voor beheer van externe gebruikers
De klasse voor beheer van externe gebruikers, met de naam MyToolWindowContent, is eenvoudig:
namespace MyToolWindowExtension;
using Microsoft.VisualStudio.Extensibility.UI;
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: null)
{
}
}
U hebt nog geen gegevenscontext nodig, u kunt het voorlopig instellen op null.
Een klasse die wordt uitgebreid RemoteUserControl , maakt automatisch gebruik van de ingesloten XAML-resource met dezelfde naam. Als u dit gedrag wilt wijzigen, overschrijft u de GetXamlAsync methode.
XAML-definitie
Maak vervolgens een bestand met de naam MyToolWindowContent.xaml:
<DataTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vs="http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml">
<Label>Hello World</Label>
</DataTemplate>
De XAML-definitie van de remote-gebruikersinterface is de normale WPF XAML die een DataTemplate beschrijft. Deze XAML wordt verzonden naar Visual Studio en wordt gebruikt om de inhoud van het taakvenster te vullen. We gebruiken een speciale naamruimte (xmlns kenmerk) voor Remote UI XAML: http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml.
XAML instellen als een ingesloten resource
Open tot slot het .csproj bestand en zorg ervoor dat het XAML-bestand wordt behandeld als een ingesloten resource:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Zoals eerder beschreven, moet het XAML-bestand dezelfde naam hebben als de klasse voor extern gebruikersbeheer . Om precies te zijn, moet de volledige naam van de klasse-uitbreiding RemoteUserControl overeenkomen met de naam van de ingesloten resource. Als de volledige naam van de klasse voor beheer van externe gebruikers bijvoorbeeld is MyToolWindowExtension.MyToolWindowContent, moet de naam van de ingesloten resource zijn MyToolWindowExtension.MyToolWindowContent.xaml. Standaard krijgen ingesloten resources een naam toegewezen die is samengesteld uit de hoofdnaamruimte van het project, het pad van eventuele submappen en hun bestandsnaam. Dit kan problemen veroorzaken als uw klasse voor extern gebruikersbeheer een andere naamruimte gebruikt dan de hoofdnaamruimte van het project of als het xaml-bestand zich niet in de hoofdmap van het project bevindt. Indien nodig kunt u een naam voor de ingesloten resource afdwingen met behulp van de LogicalName tag:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
De extensie testen
U moet nu op de knop kunnen drukken F5 om fouten in de extensie op te sporen.
Ondersteuning voor thema's toevoegen
Het is een goed idee om de gebruikersinterface te schrijven, rekening houdend met het feit dat Visual Studio kan worden gethemateerd, wat resulteert in verschillende kleuren die worden gebruikt.
Werk de XAML bij om de stijlen en kleuren te gebruiken die in Visual Studio worden gebruikt:
<DataTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vs="http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml"
xmlns:styles="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
xmlns:colors="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0">
<Grid>
<Grid.Resources>
<Style TargetType="Label" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogLabelStyleKey}}" />
</Grid.Resources>
<Label>Hello World</Label>
</Grid>
</DataTemplate>
Het label gebruikt nu hetzelfde thema als de rest van de Visual Studio-gebruikersinterface en verandert automatisch de kleur wanneer de gebruiker overschakelt naar de donkere modus:
Hier verwijst het xmlns kenmerk naar de assembly Microsoft.VisualStudio.Shell.15.0 , die geen van de uitbreidingsafhankelijkheden is. Dit is prima omdat deze XAML wordt gebruikt door het Visual Studio-proces, dat afhankelijk is van Shell.15, niet door de extensie zelf.
Om een betere XAML-bewerkingservaring te krijgen, kunt u tijdelijk een PackageReference aan Microsoft.VisualStudio.Shell.15.0 het extensieproject toevoegen.
Vergeet niet om het later te verwijderen omdat een out-of-process VisualStudio.Extensibility-extensie niet naar dit pakket mag verwijzen.
Een gegevenscontext toevoegen
Voeg een gegevenscontextklasse toe voor het beheer van externe gebruikers:
using System.Runtime.Serialization;
namespace MyToolWindowExtension;
[DataContract]
internal class MyToolWindowData
{
[DataMember]
public string? LabelText { get; init; }
}
Werk vervolgens MyToolWindowContent.cs en MyToolWindowContent.xaml bij om dit te gebruiken:
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData { LabelText = "Hello Binding!"})
{
}
<Label Content="{Binding LabelText}" />
De inhoud van het label wordt nu ingesteld via gegevensbinding:
Het gegevenscontexttype hier is gemarkeerd met DataContract en DataMember kenmerken. Dit komt doordat het MyToolWindowData exemplaar bestaat in het extensiehostproces, terwijl het WPF-besturingselement dat is gemaakt van MyToolWindowContent.xaml in het Visual Studio-proces bestaat. Om gegevensbinding te laten werken, genereert de remote UI-infrastructuur een proxy van het MyToolWindowData object in het Visual Studio-proces. De DataContract en DataMember kenmerken geven aan welke typen en eigenschappen relevant zijn voor gegevensbinding en moeten worden gerepliceerd in de proxy.
De gegevenscontext van het beheer van externe gebruikers wordt doorgegeven als een constructorparameter van de RemoteUserControl klasse: de RemoteUserControl.DataContext eigenschap heeft het kenmerk Alleen-lezen. Dit impliceert niet dat de hele gegevenscontext onveranderbaar is, maar het contextobject van de hoofdgegevens van een extern gebruikersbeheer kan niet worden vervangen. In de volgende sectie maken MyToolWindowData we veranderlijk en waarneembaar.
Serialiseerbare typen en externe UI-gegevenscontext
Een externe UI-gegevenscontext kan alleen serialiseerbare typen bevatten of, om specifieker te zijn, alleen DataMember eigenschappen van een serialiseerbaar type kunnen worden gebonden aan gegevens.
Alleen de volgende types kunnen worden geserialiseerd door Remote UI:
- primitieve gegevens (de meeste .NET-numerieke typen, opsommingen,
bool, ,stringDateTime) - extender-gedefinieerde typen die zijn gemarkeerd met
DataContractenDataMemberkenmerken (en al hun gegevensleden zijn ook serialiseerbaar) - objecten die IAsyncCommand implementeren
- XamlFragment- en SolidColorBrush-objecten en kleurwaarden
-
Nullable<>waarden voor een serialiseerbare type - verzamelingen van serialiseerbare typen, waaronder waarneembare verzamelingen.
Levenscyclus van een extern gebruikersbeheer
U kunt de ControlLoadedAsync methode overschrijven om te worden gewaarschuwd wanneer het besturingselement voor het eerst wordt geladen in een WPF-container. Als in uw implementatie de status van de gegevenscontext onafhankelijk van ui-gebeurtenissen kan veranderen, is de ControlLoadedAsync methode de juiste plek om de inhoud van de gegevenscontext te initialiseren en wijzigingen erop toe te passen.
U kunt ook de Dispose-methode overschrijven om te worden geïnformeerd wanneer het besturingselement is vernietigd en niet meer zal worden gebruikt.
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData())
{
}
public override async Task ControlLoadedAsync(CancellationToken cancellationToken)
{
await base.ControlLoadedAsync(cancellationToken);
// Your code here
}
protected override void Dispose(bool disposing)
{
// Your code here
base.Dispose(disposing);
}
}
Opdrachten, waarneembaarheid en gegevensbinding in twee richtingen
Laten we vervolgens de gegevenscontext waarneembaar maken en een knop toevoegen aan de werkset.
De gegevenscontext kan waarneembaar worden gemaakt door INotifyPropertyChanged te implementeren. Remote UI biedt ook een handige abstracte klasse, NotifyPropertyChangedObjectdie we kunnen uitbreiden om standaardcode te verminderen.
Een gegevenscontext heeft meestal een combinatie van leeseigenschappen en waarneembare eigenschappen. De gegevenscontext kan een complex graf van objecten zijn zolang ze zijn gemarkeerd met de DataContract en DataMember kenmerken en INotifyPropertyChanged implementeren als dat nodig is. Het is ook mogelijk om waarneembare verzamelingen of een ObservableList<T> te hebben, een uitgebreide ObservableCollection<T> die wordt geleverd door de externe gebruikersinterface om ook bereikbewerkingen te ondersteunen, waardoor betere prestaties mogelijk zijn.
We moeten ook een opdracht toevoegen aan de gegevenscontext. In de Remote UI worden IAsyncCommand instructies uitgevoerd, maar het is vaak eenvoudiger om een exemplaar van de AsyncCommand klasse te maken.
IAsyncCommand verschilt ICommand op twee manieren:
- De
Executemethode wordt vervangen doorExecuteAsyncomdat alles in de externe gebruikersinterface asynchroon is. - De
CanExecute(object)methode wordt vervangen door eenCanExecuteeigenschap. DeAsyncCommandklasse zorgt ervoor dat waarneembaar wordtCanExecute.
Het is belangrijk te weten dat de Remote gebruikersinterface geen ondersteuning biedt voor event handlers, dus alle meldingen van de gebruikersinterface naar de extensie moeten worden geïmplementeerd via databinding en commando's.
Dit is de resulterende code voor MyToolWindowData:
[DataContract]
internal class MyToolWindowData : NotifyPropertyChangedObject
{
public MyToolWindowData()
{
HelloCommand = new((parameter, cancellationToken) =>
{
Text = $"Hello {Name}!";
return Task.CompletedTask;
});
}
private string _name = string.Empty;
[DataMember]
public string Name
{
get => _name;
set => SetProperty(ref this._name, value);
}
private string _text = string.Empty;
[DataMember]
public string Text
{
get => _text;
set => SetProperty(ref this._text, value);
}
[DataMember]
public AsyncCommand HelloCommand { get; }
}
Herstel de MyToolWindowContent constructor:
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData())
{
}
Werk MyToolWindowContent.xaml bij om de nieuwe eigenschappen in de gegevenscontext te gebruiken. Dit is allemaal normaal WPF XAML. Zelfs het IAsyncCommand object wordt geopend via een proxy die wordt aangeroepen ICommand in het Visual Studio-proces, zodat het gegevensgebonden kan zijn zoals gebruikelijk.
<DataTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vs="http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml"
xmlns:styles="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
xmlns:colors="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0">
<Grid>
<Grid.Resources>
<Style TargetType="Label" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogLabelStyleKey}}" />
<Style TargetType="TextBox" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.TextBoxStyleKey}}" />
<Style TargetType="Button" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ButtonStyleKey}}" />
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource {x:Static styles:VsBrushes.WindowTextKey}}" />
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Content="Name:" />
<TextBox Text="{Binding Name}" Grid.Column="1" />
<Button Content="Say Hello" Command="{Binding HelloCommand}" Grid.Column="2" />
<TextBlock Text="{Binding Text}" Grid.ColumnSpan="2" Grid.Row="1" />
</Grid>
</DataTemplate>
Inzicht in asynchroniciteit in de externe gebruikersinterface
De volledige communicatie van de externe gebruikersinterface voor dit hulpprogrammavenster verloopt volgens de volgende stappen:
De gegevenscontext wordt geopend via een proxy in het Visual Studio-proces met de oorspronkelijke inhoud,
Het besturingselement dat is gemaakt op basis van
MyToolWindowContent.xamlgegevens, is gebonden aan de gegevenscontextproxy.De gebruiker typt tekst in het tekstvak, die wordt toegewezen aan de
Nameeigenschap van de gegevenscontext-proxy via databinding. De nieuwe waarde vanNamewordt doorgegeven aan hetMyToolWindowDataobject.De gebruiker klikt op de knop die een trapsgewijs effect veroorzaakt:
- In de gegevenscontextproxy wordt
HelloCommanduitgevoerd - de asynchrone uitvoering van de extendercode
AsyncCommandwordt gestart - de asynchrone callback voor
HelloCommandwerkt de waarde van de waarneembare eigenschapTextbij - de nieuwe waarde van
Textwordt doorgegeven aan de gegevenscontextproxy - het tekstblok in het taakvenster wordt bijgewerkt naar de nieuwe waarde van
Textvia gegevensbinding
- In de gegevenscontextproxy wordt
Opdrachtparameters gebruiken om racevoorwaarden te voorkomen
Alle bewerkingen die betrekking hebben op communicatie tussen Visual Studio en de extensie (blauwe pijlen in het diagram) zijn asynchroon. Het is belangrijk om dit aspect in het algehele ontwerp van de extensie te overwegen.
Als consistentie daarom belangrijk is, is het beter om opdrachtparameters te gebruiken in plaats van binding in twee richtingen om de status van de gegevenscontext op te halen op het moment van de uitvoering van een opdracht.
Breng deze wijziging aan door de knop CommandParameter te binden aan Name:
<Button Content="Say Hello" Command="{Binding HelloCommand}" CommandParameter="{Binding Name}" Grid.Column="2" />
Wijzig vervolgens de callback van de opdracht om de parameter te gebruiken:
HelloCommand = new AsyncCommand((parameter, cancellationToken) =>
{
Text = $"Hello {(string)parameter!}!";
return Task.CompletedTask;
});
Met deze methode wordt de waarde van de Name eigenschap synchroon opgehaald uit de gegevenscontextproxy op het moment van de knopklik en verzonden naar de extensie. Dit voorkomt racevoorwaarden, met name als de HelloCommand callback in de toekomst wordt gewijzigd in rendement (met await expressies).
Async-opdrachten verbruiken gegevens uit meerdere eigenschappen
Het gebruik van een opdrachtparameter is geen optie als de opdracht meerdere eigenschappen moet gebruiken die door de gebruiker zijn ingesteld. Als de gebruikersinterface bijvoorbeeld twee tekstvaken heeft: 'Voornaam' en 'Achternaam'.
De oplossing in dit geval is om in de callback van de asynchrone opdracht de waarde van alle eigenschappen uit de gegevenscontext op te halen voordat u het resultaat krijgt.
Hieronder ziet u een voorbeeld waarin de FirstName en LastName eigenschapswaarden worden opgehaald voordat ze worden doorgegeven, zodat de waarde op het moment van het uitvoeren van de opdracht wordt benut.
HelloCommand = new(async (parameter, cancellationToken) =>
{
string firstName = FirstName;
string lastName = LastName;
await Task.Delay(TimeSpan.FromSeconds(1));
Text = $"Hello {firstName} {lastName}!";
});
Het is ook belangrijk om te voorkomen dat de extensie asynchroon de waarde bijwerkt van eigenschappen die gebruikers ook kunnen bijwerken. Met andere woorden, vermijd TwoWay-gegevensbinding .
Verwante inhoud
De informatie hier moet voldoende zijn om eenvoudige remote UI-onderdelen te bouwen. Zie Andere concepten van de externe gebruikersinterface voor aanvullende onderwerpen met betrekking tot het werken met het externe UI-model. Zie Geavanceerde concepten van de externe gebruikersinterface voor meer geavanceerde scenario's.