Mengapa Antarmuka Pengguna Jarak Jauh
Salah satu tujuan utama model VisualStudio.Extensibility adalah untuk memungkinkan ekstensi berjalan di luar proses Visual Studio. Ini memperkenalkan hambatan untuk menambahkan dukungan UI ke ekstensi karena sebagian besar kerangka kerja UI sedang dalam proses.
Antarmuka pengguna jarak jauh adalah sekumpulan kelas yang memungkinkan Anda menentukan kontrol WPF dalam ekstensi di luar proses dan menampilkannya sebagai bagian dari UI Visual Studio.
Antarmuka pengguna jarak jauh sangat bergantung pada pola desain Model-View-ViewModel yang mengandalkan XAML dan pengikatan data, perintah (bukan peristiwa), dan pemicu (alih-alih berinteraksi dengan pohon logis dari code-behind).
Sementara Antarmuka Pengguna Jarak Jauh dikembangkan untuk mendukung ekstensi di luar proses, API VisualStudio.Extensibility yang mengandalkan Antarmuka Pengguna Jarak Jauh, seperti ToolWindow
, akan menggunakan Antarmuka Pengguna Jarak Jauh untuk ekstensi dalam proses juga.
Perbedaan utama antara Antarmuka Pengguna Jarak Jauh dan pengembangan WPF normal adalah:
- Sebagian besar operasi Antarmuka Pengguna Jarak Jauh, termasuk pengikatan ke konteks data dan eksekusi perintah, bersifat asinkron.
- Saat menentukan jenis data yang akan digunakan dalam konteks data Antarmuka Pengguna Jarak Jauh, jenis data harus didekorasi dengan
DataContract
atribut danDataMember
dan jenisnya harus dapat diserialisasikan oleh Antarmuka Pengguna Jarak Jauh (lihat di sini untuk detailnya). - Antarmuka pengguna jarak jauh tidak mengizinkan referensi kontrol kustom Anda sendiri.
- Kontrol pengguna Jarak Jauh sepenuhnya ditentukan dalam satu file XAML yang mereferensikan satu objek konteks data (tetapi berpotensi kompleks dan berlapis).
- Antarmuka pengguna jarak jauh tidak mendukung kode di belakang atau penanganan aktivitas (solusi dijelaskan dalam dokumen konsep Antarmuka Pengguna Jarak Jauh tingkat lanjut).
- Kontrol pengguna Jarak Jauh dibuat dalam proses Visual Studio, bukan proses yang menghosting ekstensi: XAML tidak dapat mereferensikan jenis dan rakitan dari ekstensi tetapi dapat mereferensikan jenis dan rakitan dari proses Visual Studio.
Membuat ekstensi Halo Dunia antarmuka pengguna jarak jauh
Mulailah dengan membuat ekstensi Antarmuka Pengguna Jarak Jauh paling dasar. Ikuti petunjuk dalam Membuat ekstensi Visual Studio pertama Anda yang tidak diproses.
Anda sekarang harus memiliki ekstensi yang berfungsi dengan satu perintah, langkah selanjutnya adalah menambahkan ToolWindow
dan RemoteUserControl
. RemoteUserControl
adalah antarmuka pengguna Jarak Jauh yang setara dengan kontrol pengguna WPF.
Anda akan berakhir dengan empat file:
.cs
file untuk perintah yang membuka jendela alat,.cs
file untukToolWindow
yang menyediakanRemoteUserControl
ke Visual Studio,.cs
file untukRemoteUserControl
yang mereferensikan definisi XAML-nya,.xaml
file untukRemoteUserControl
.
Kemudian, Anda menambahkan konteks data untuk RemoteUserControl
, yang mewakili ViewModel dalam pola MVVM.
Perbarui perintah
Perbarui kode perintah untuk menampilkan jendela alat menggunakan ShowToolWindowAsync
:
public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}
Anda juga dapat mempertimbangkan untuk mengubah CommandConfiguration
dan string-resources.json
untuk pesan tampilan dan penempatan yang lebih tepat:
public override CommandConfiguration CommandConfiguration => new("%MyToolWindowCommand.DisplayName%")
{
Placements = new[] { CommandPlacement.KnownPlacements.ViewOtherWindowsMenu },
};
{
"MyToolWindowCommand.DisplayName": "My Tool Window"
}
Membuat jendela alat
Buat file baru MyToolWindow.cs
dan tentukan kelas yang MyToolWindow
diperluas ToolWindow
.
Metode GetContentAsync
ini seharusnya mengembalikan IRemoteUserControl
yang akan Anda tentukan di langkah berikutnya. Karena kontrol pengguna jarak jauh sekali pakai, berhati-hatilah untuk membuangnya dengan mengesampingkan Dispose(bool)
metode .
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);
}
}
Membuat kontrol pengguna jarak jauh
Lakukan tindakan ini di tiga file:
Kelas kontrol pengguna jarak jauh
Kelas kontrol pengguna jarak jauh, bernama MyToolWindowContent
, mudah:
namespace MyToolWindowExtension;
using Microsoft.VisualStudio.Extensibility.UI;
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: null)
{
}
}
Anda belum memerlukan konteks data, sehingga Anda dapat mengaturnya untuk null
saat ini.
Kelas yang diperluas RemoteUserControl
secara otomatis menggunakan sumber daya yang disematkan XAML dengan nama yang sama. Jika Anda ingin mengubah perilaku ini, ambil alih GetXamlAsync
metode .
Definisi XAML
Selanjutnya, buat file bernama 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>
Definisi XAML dari kontrol pengguna jarak jauh adalah WPF XAML normal yang DataTemplate
menjelaskan . XAML ini dikirim ke Visual Studio dan digunakan untuk mengisi konten jendela alat. Kami menggunakan namespace khusus (xmlns
atribut) untuk Remote UI XAML: http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml
.
Mengatur XAML sebagai sumber daya yang disematkan
Terakhir, buka .csproj
file dan pastikan bahwa file XAML diperlakukan sebagai sumber daya yang disematkan:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Seperti yang dijelaskan sebelumnya, file XAML harus memiliki nama yang sama dengan kelas kontrol pengguna jarak jauh. Tepatnya, nama lengkap perpanjangan RemoteUserControl
kelas harus cocok dengan nama sumber daya yang disematkan. Misalnya, jika nama lengkap kelas kontrol pengguna jarak jauh adalah MyToolWindowExtension.MyToolWindowContent
, nama sumber daya yang disematkan harus MyToolWindowExtension.MyToolWindowContent.xaml
. Secara default, sumber daya yang disematkan diberi nama yang terdiri dari namespace layanan akar untuk proyek, jalur subfolder apa pun yang mungkin ada di bawahnya, dan nama filenya. Ini dapat membuat masalah jika kelas kontrol pengguna jarak jauh Anda menggunakan namespace yang berbeda dari namespace layanan akar proyek atau jika file xaml tidak ada di folder akar proyek. Jika perlu, Anda dapat memaksa nama untuk sumber daya yang disematkan dengan menggunakan LogicalName
tag:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Menguji ekstensi
Anda sekarang dapat menekan F5
untuk men-debug ekstensi.
Menambahkan dukungan untuk tema
ada baiknya untuk menulis UI yang perlu diingat bahwa Visual Studio dapat bertema menghasilkan warna yang berbeda yang digunakan.
Perbarui XAML untuk menggunakan gaya dan warna yang digunakan di seluruh Visual Studio:
<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>
Label sekarang menggunakan tema yang sama dengan UI Visual Studio lainnya, dan secara otomatis berubah warna saat pengguna beralih ke mode gelap:
Di sini, atribut mereferensikan xmlns
rakitan Microsoft.VisualStudio.Shell.15.0 , yang bukan salah satu dependensi ekstensi. Ini tidak masalah karena XAML ini digunakan oleh proses Visual Studio, yang memiliki dependensi pada Shell.15, bukan oleh ekstensi itu sendiri.
Untuk mendapatkan pengalaman pengeditan XAML yang lebih baik, Anda dapat menambahkan PackageReference
untuk sementara ke Microsoft.VisualStudio.Shell.15.0
proyek ekstensi. Jangan lupa untuk menghapusnya nanti karena ekstensi VisualStudio.Extensibility di luar proses tidak boleh mereferensikan paket ini!
Menambahkan konteks data
Tambahkan kelas konteks data untuk kontrol pengguna jarak jauh:
using System.Runtime.Serialization;
namespace MyToolWindowExtension;
[DataContract]
internal class MyToolWindowData
{
[DataMember]
public string? LabelText { get; init; }
}
dan perbarui MyToolWindowContent.cs
dan MyToolWindowContent.xaml
untuk menggunakannya:
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData { LabelText = "Hello Binding!"})
{
}
<Label Content="{Binding LabelText}" />
Konten label sekarang diatur melalui pengikatan data:
Jenis konteks data di sini ditandai dengan DataContract
atribut dan DataMember
. Ini karena MyToolWindowData
instans ada dalam proses host ekstensi saat kontrol WPF yang dibuat dari MyToolWindowContent.xaml
ada dalam proses Visual Studio. Untuk membuat pengikatan data berfungsi, infrastruktur Antarmuka Pengguna Jarak Jauh menghasilkan proksi MyToolWindowData
objek dalam proses Visual Studio. Atribut DataContract
dan DataMember
menunjukkan jenis dan properti mana yang relevan untuk pengikatan data dan harus direplikasi dalam proksi.
Konteks data kontrol pengguna jarak jauh diteruskan sebagai parameter RemoteUserControl
konstruktor kelas: RemoteUserControl.DataContext
properti bersifat baca-saja. Ini tidak menyiratkan bahwa seluruh konteks data tidak dapat diubah, tetapi objek konteks data akar kontrol pengguna jarak jauh tidak dapat diganti. Di bagian berikutnya, kita akan membuat MyToolWindowData
mutable dan dapat diamati.
Jenis yang dapat diserialisasikan dan konteks data Antarmuka Pengguna Jarak Jauh
Konteks data Antarmuka Pengguna Jarak Jauh hanya dapat berisi jenis yang dapat diserialisasikan atau, agar lebih tepat, hanya DataMember
properti dari jenis yang dapat diserialisasikan yang dapat diikat data.
Hanya jenis berikut yang dapat diserialisasikan oleh Antarmuka Pengguna Jarak Jauh:
- data primitif (sebagian besar jenis numerik .NET, enum,
bool
,string
,DateTime
) - jenis yang ditentukan extender yang ditandai dengan
DataContract
atribut danDataMember
(dan semua anggota data mereka juga dapat diserialisasikan) - objek yang mengimplementasikan IAsyncCommand
- Objek XamlFragment, dan SolidColorBrush, dan Nilai warna
Nullable<>
nilai untuk jenis yang dapat diserialisasikan- koleksi jenis yang dapat diserialisasikan, termasuk koleksi yang dapat diamati.
Siklus Hidup Kontrol Pengguna Jarak Jauh
Anda dapat mengambil alih metode yang ControlLoadedAsync
akan diberi tahu ketika kontrol pertama kali dimuat dalam kontainer WPF. Jika dalam implementasi Anda, status konteks data dapat berubah secara independen dari peristiwa UI, ControlLoadedAsync
metode ini adalah tempat yang tepat untuk menginisialisasi konten konteks data dan mulai menerapkan perubahan padanya.
Anda juga dapat mengambil alih metode yang Dispose
akan diberi tahu ketika kontrol dihancurkan dan tidak akan digunakan lagi.
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);
}
}
Perintah, pengamatan, dan pengikatan data dua arah
Selanjutnya, mari kita buat konteks data dapat diamati dan menambahkan tombol ke kotak alat.
Konteks data dapat diamati dengan menerapkan INotifyPropertyChanged. Atau, Antarmuka pengguna Jarak Jauh menyediakan kelas abstrak yang nyaman, NotifyPropertyChangedObject
, yang dapat kita perluas untuk mengurangi kode boilerplate.
Konteks data biasanya memiliki campuran properti baca-saja dan properti yang dapat diamati. Konteks data dapat menjadi grafik objek yang kompleks selama ditandai dengan DataContract
atribut dan dan DataMember
menerapkan INotifyPropertyChanged seperlunya . Anda juga dapat memiliki koleksi yang dapat diamati, atau ObservableList<T>, yang merupakan ObservableCollection<T> yang diperluas yang disediakan oleh Antarmuka Pengguna Jarak Jauh untuk juga mendukung operasi rentang, memungkinkan performa yang lebih baik.
Kita juga perlu menambahkan perintah ke konteks data. Di Antarmuka pengguna Jarak Jauh, perintah diterapkan IAsyncCommand
tetapi sering kali lebih mudah untuk membuat instans AsyncCommand
kelas.
IAsyncCommand
berbeda dari ICommand
dalam dua cara:
- Metode
Execute
ini diganti denganExecuteAsync
karena semua yang ada di Antarmuka Pengguna Jarak Jauh adalah asinkron! - Metode
CanExecute(object)
digantikan olehCanExecute
properti. KelasAsyncCommand
mengurus membuatCanExecute
pengamatan.
Penting untuk dicatat bahwa Antarmuka Pengguna Jarak Jauh tidak mendukung penanganan aktivitas, sehingga semua pemberitahuan dari UI ke ekstensi harus diimplementasikan melalui pengikatan data dan perintah.
Ini adalah kode yang dihasilkan untuk 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; }
}
MyToolWindowContent
Perbaiki konstruktor:
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData())
{
}
Perbarui MyToolWindowContent.xaml
untuk menggunakan properti baru dalam konteks data. Ini semua WPF XAML normal. IAsyncCommand
Bahkan objek diakses melalui proksi yang disebut ICommand
dalam proses Visual Studio sehingga dapat terikat data seperti biasa.
<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>
Memahami asinkron di Antarmuka Pengguna Jarak Jauh
Seluruh komunikasi Antarmuka Pengguna Jarak Jauh untuk jendela alat ini mengikuti langkah-langkah berikut:
Konteks data diakses melalui proksi di dalam proses Visual Studio dengan konten aslinya,
Kontrol yang dibuat dari
MyToolWindowContent.xaml
adalah data yang terikat ke proksi konteks data,Pengguna mengetikkan beberapa teks dalam kotak teks, yang ditetapkan ke
Name
properti proksi konteks data melalui pengikatan data. Nilai baru disebarkanName
keMyToolWindowData
objek.Pengguna mengklik tombol yang menyebabkan kaskade efek:
HelloCommand
dalam proksi konteks data dijalankan- eksekusi asinkron kode extender
AsyncCommand
dimulai - panggilan balik asinkron untuk
HelloCommand
memperbarui nilai properti yang dapat diamatiText
- nilai baru disebarkan
Text
ke proksi konteks data - blok teks di jendela alat diperbarui ke nilai
Text
baru melalui pengikatan data
Menggunakan parameter perintah untuk menghindari kondisi balapan
Semua operasi yang melibatkan komunikasi antara Visual Studio dan ekstensi (panah biru dalam diagram) tidak sinkron. Penting untuk mempertimbangkan aspek ini dalam desain keseluruhan ekstensi.
Untuk alasan ini, jika konsistensi penting, lebih baik menggunakan parameter perintah, alih-alih pengikatan dua arah, untuk mengambil status konteks data pada saat eksekusi perintah.
Lakukan perubahan ini dengan mengikat tombol CommandParameter
ke Name
:
<Button Content="Say Hello" Command="{Binding HelloCommand}" CommandParameter="{Binding Name}" Grid.Column="2" />
Kemudian, ubah panggilan balik perintah untuk menggunakan parameter :
HelloCommand = new AsyncCommand((parameter, cancellationToken) =>
{
Text = $"Hello {(string)parameter!}!";
return Task.CompletedTask;
});
Dengan pendekatan ini, nilai Name
properti diambil secara sinkron dari proksi konteks data pada saat klik tombol dan dikirim ke ekstensi. Ini menghindari kondisi balapan apa pun, terutama jika HelloCommand
panggilan balik diubah di masa depan untuk menghasilkan (memiliki await
ekspresi).
Perintah asinkron menggunakan data dari beberapa properti
Menggunakan parameter perintah bukanlah opsi jika perintah perlu menggunakan beberapa properti yang dapat diatur oleh pengguna. Misalnya, jika UI memiliki dua kotak teks: "Nama Depan" dan "Nama Belakang".
Solusi dalam hal ini adalah mengambil, dalam panggilan balik perintah asinkron, nilai semua properti dari konteks data sebelum menghasilkan.
Di bawah ini Anda dapat melihat sampel tempat FirstName
nilai properti dan LastName
diambil sebelum menghasilkan untuk memastikan bahwa nilai pada saat pemanggilan perintah digunakan:
HelloCommand = new(async (parameter, cancellationToken) =>
{
string firstName = FirstName;
string lastName = LastName;
await Task.Delay(TimeSpan.FromSeconds(1));
Text = $"Hello {firstName} {lastName}!";
});
Penting juga untuk menghindari ekstensi memperbarui nilai properti secara asinkron yang juga dapat diperbarui oleh pengguna. Dengan kata lain, hindari pengikatan data TwoWay .
Konten terkait
Informasi di sini harus cukup untuk membangun komponen Antarmuka Pengguna Jarak Jauh sederhana. Untuk skenario yang lebih canggih, lihat Konsep UI Jarak Jauh Tingkat Lanjut.