Neden Uzak Kullanıcı Arabirimi

VisualStudio.Genişletilebilirlik modelinin ana hedeflerinden biri, uzantıların Visual Studio işleminin dışında çalışmasına izin vermektir. Bu, çoğu kullanıcı arabirimi çerçevesi devam ettiğinden uzantılara kullanıcı arabirimi desteği eklemeye engel oluşturur.

Uzak kullanıcı arabirimi, işlem dışı bir uzantıda WPF denetimleri tanımlamanıza ve bunları Visual Studio kullanıcı arabiriminin bir parçası olarak göstermenize olanak sağlayan bir sınıf kümesidir.

Uzak kullanıcı arabirimi, XAML ve veri bağlama, komutlar (olaylar yerine) ve tetikleyicileri (arka planda bulunan mantıksal ağaçla etkileşime geçmek yerine) kullanan Model-View-ViewModel tasarım desenine büyük ölçüde dayanır.

Uzak kullanıcı arabirimi, işlem dışı uzantıları destekleyecek şekilde geliştirilmiş olsa da, gibi ToolWindowUzak kullanıcı arabirimini kullanan VisualStudio.Genişletilebilirlik API'leri de işlem içi uzantılar için Uzak Kullanıcı Arabirimi'ni kullanır.

Uzak kullanıcı arabirimi ile normal WPF geliştirme arasındaki temel farklar şunlardır:

  • Veri bağlamı ve komut yürütme bağlama da dahil olmak üzere çoğu Uzak UI işlemi zaman uyumsuz olur.
  • Uzak UI veri bağlamlarında kullanılacak veri türlerini tanımlarken, ve DataMember öznitelikleriyle DataContract ve türleri Uzak Kullanıcı Arabirimi tarafından serileştirilebilir olmalıdır (ayrıntılar için buraya bakın).
  • Uzak kullanıcı arabirimi kendi özel denetimlerinize başvurmaya izin vermez.
  • Uzak kullanıcı denetimi, tek bir (ancak karmaşık ve iç içe geçmiş) veri bağlam nesnesine başvuran tek bir XAML dosyasında tam olarak tanımlanır.
  • Uzak kullanıcı arabirimi arka planda kodu veya olay işleyicilerini desteklemez (geçici çözümler gelişmiş Uzak KULLANıCı arabirimi kavramları belgesinde açıklanmıştır).
  • Uzak kullanıcı denetimi, uzantıyı barındıran işlemde değil Visual Studio işleminde örneği oluşturulur: XAML uzantıdan türlere ve derlemelere başvuramaz, ancak Visual Studio işleminden türlere ve derlemelere başvurabilir.

Uzak kullanıcı arabirimi Merhaba Dünya uzantısı oluşturma

En temel Uzak Kullanıcı Arabirimi uzantısını oluşturarak başlayın. İlk işlem dışı Visual Studio uzantınızı oluşturma başlığındaki yönergeleri izleyin.

Artık tek bir komutla çalışan bir uzantıya sahip olmanız gerekir. Sonraki adım ve ToolWindowRemoteUserControleklemektir. RemoteUserControl, WPF kullanıcı denetiminin Uzak kullanıcı arabirimi eşdeğeridir.

Dört dosyayla sonuçlanır:

  1. araç penceresini açan komut için bir .cs dosya,
  2. için Visual Studio'ya sağlayan RemoteUserControl bir .cs dosyaToolWindow,
  3. için XAML tanımına RemoteUserControl başvuran bir .cs dosya,
  4. için RemoteUserControlbir .xaml dosya.

Daha sonra, MVVM desenindeki ViewModel'i temsil eden için RemoteUserControlbir veri bağlamı eklersiniz.

Komutu güncelleştirme

komutunun ShowToolWindowAsynckodunu kullanarak araç penceresini gösterecek şekilde güncelleştirin:

public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
    return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}

Ayrıca, daha uygun bir görüntüleme iletisi ve yerleşimi için ve string-resources.json değişikliklerini CommandConfiguration de göz önünde bulundurabilirsiniz:

public override CommandConfiguration CommandConfiguration => new("%MyToolWindowCommand.DisplayName%")
{
    Placements = new[] { CommandPlacement.KnownPlacements.ViewOtherWindowsMenu },
};
{
  "MyToolWindowCommand.DisplayName": "My Tool Window"
}

Araç penceresini oluşturma

Yeni MyToolWindow.cs bir dosya oluşturun ve genişleten ToolWindowbir MyToolWindow sınıf tanımlayın.

yönteminin GetContentAsync bir sonraki adımda tanımlayabileceğiniz bir IRemoteUserControl döndürmesi gerekir. Uzak kullanıcı denetimi atılabilir olduğundan, yöntemini geçersiz kılarak kullanımdan kaldırmaya Dispose(bool) dikkat edin.

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

Uzak kullanıcı denetimini oluşturma

Bu eylemi üç dosyada gerçekleştirin:

Uzak kullanıcı denetimi sınıfı

adlı MyToolWindowContentuzak kullanıcı denetimi sınıfı basittir:

namespace MyToolWindowExtension;

using Microsoft.VisualStudio.Extensibility.UI;

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: null)
    {
    }
}

Henüz bir veri bağlamı gerekmez, bu nedenle şimdilik olarak ayarlayabilirsiniz null .

Genişleten RemoteUserControl bir sınıf otomatik olarak aynı ada sahip XAML eklenmiş kaynağını kullanır. Bu davranışı değiştirmek istiyorsanız yöntemini geçersiz kılın GetXamlAsync .

XAML tanımı

Ardından adlı MyToolWindowContent.xamlbir dosya oluşturun:

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

Uzak kullanıcı denetiminin XAML tanımı, bir açıklayan normal WPF XAML'dir DataTemplate. Bu XAML Visual Studio'ya gönderilir ve araç penceresi içeriğini doldurmak için kullanılır. Uzak UI XAML için özel bir ad alanı (xmlns öznitelik) kullanırız: http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml.

XAML'yi ekli kaynak olarak ayarlama

Son olarak, dosyayı açın .csproj ve XAML dosyasının katıştırılmış kaynak olarak işlendiğinden emin olun:

<ItemGroup>
  <EmbeddedResource Include="MyToolWindowContent.xaml" />
  <Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>

Daha önce açıklandığı gibi, XAML dosyasının uzak kullanıcı denetimi sınıfıyla aynı ada sahip olması gerekir. Kesin olarak, genişleten RemoteUserControl sınıfın tam adı eklenmiş kaynağın adıyla eşleşmelidir. Örneğin, uzak kullanıcı denetim sınıfının tam adı iseMyToolWindowExtension.MyToolWindowContent, eklenmiş kaynak adı olmalıdırMyToolWindowExtension.MyToolWindowContent.xaml. Varsayılan olarak, katıştırılmış kaynaklara projenin kök ad alanı tarafından oluşturulan bir ad, altında olabilecekleri alt klasör yolları ve dosya adları atanır. Bu, uzak kullanıcı denetim sınıfınız projenin kök ad alanından farklı bir ad alanı kullanıyorsa veya xaml dosyası projenin kök klasöründe değilse sorun oluşturabilir. Gerekirse, etiketini kullanarak LogicalName eklenmiş kaynak için bir ad zorlayabilirsiniz:

<ItemGroup>
  <EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
  <Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>

Uzantıyı test etme

Artık uzantıda hata ayıklamak için basabilmeniz F5 gerekir.

Menü ve araç penceresini gösteren ekran görüntüsü.

Temalar için destek ekleme

Visual Studio'nun tema oluşturabileceğini ve farklı renklerin kullanılmasına neden olabileceğini göz önünde bulundurarak kullanıcı arabirimini yazmak iyi bir fikirdir.

Visual Studio'da kullanılan stilleri ve renkleri kullanmak için XAML'yi güncelleştirin:

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

Etiket artık Visual Studio kullanıcı arabiriminin geri kalanıyla aynı temayı kullanır ve kullanıcı koyu moda geçtiğinde renk otomatik olarak değişir:

Temalı araç penceresini gösteren ekran görüntüsü.

Burada özniteliği, xmlns uzantı bağımlılıklarından biri olmayan Microsoft.VisualStudio.Shell.15.0 derlemesine başvurur. Bu XAML, uzantının kendisi tarafından değil Shell.15'e bağımlılığı olan Visual Studio işlemi tarafından kullanıldığından bu sorun yoktur.

Daha iyi bir XAML düzenleme deneyimi elde etmek için uzantı projesine Microsoft.VisualStudio.Shell.15.0 geçici olarak bir PackageReference ekleyebilirsiniz. İşlem dışı bir VisualStudio.Genişletilebilirlik uzantısı bu pakete başvurmaması gerektiğinden daha sonra kaldırmayı unutmayın!

Veri bağlamı ekleme

Uzak kullanıcı denetimi için veri bağlamı sınıfı ekleyin:

using System.Runtime.Serialization;

namespace MyToolWindowExtension;

[DataContract]
internal class MyToolWindowData
{
    [DataMember]
    public string? LabelText { get; init; }
}

ve güncelleştirin MyToolWindowContent.cs ve MyToolWindowContent.xaml kullanın:

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: new MyToolWindowData { LabelText = "Hello Binding!"})
    {
    }
<Label Content="{Binding LabelText}" />

Etiketin içeriği artık veri bağlama aracılığıyla ayarlanır:

Veri bağlamalı araç penceresini gösteren ekran görüntüsü.

Buradaki veri bağlam türü ve DataMember öznitelikleriyle DataContract işaretlenir. Bunun nedeni MyToolWindowData , örneğin uzantı ana bilgisayar işleminde mevcut olması ve içinden MyToolWindowContent.xaml oluşturulan WPF denetiminin Visual Studio işleminde mevcut olmasıdır. Veri bağlamanın çalışmasını sağlamak için, Uzak UI altyapısı Visual Studio işleminde nesnenin MyToolWindowData bir proxy'sini oluşturur. DataContract ve DataMember öznitelikleri, hangi tür ve özelliklerin veri bağlamaya uygun olduğunu ve ara sunucuya çoğaltılması gerektiğini belirtir.

Uzak kullanıcı denetiminin veri bağlamı sınıfının oluşturucu parametresi RemoteUserControl olarak geçirilir: RemoteUserControl.DataContext özelliği salt okunurdur. Bu, tüm veri bağlamının sabit olduğu anlamına gelmez, ancak uzak kullanıcı denetiminin kök veri bağlamı nesnesi değiştirilemez. Sonraki bölümde, karartılabilir ve gözlemlenebilir hale getireceğiz MyToolWindowData .

Serileştirilebilir türler ve Uzak UI veri bağlamı

Uzak UI veri bağlamı yalnızca serileştirilebilir türler içerebilir veya daha kesin olmak gerekirse yalnızca DataMember serileştirilebilir bir türün özellikleri veriye bağlanabilir.

Uzak kullanıcı arabirimi tarafından yalnızca aşağıdaki türler serileştirilebilir:

  • ilkel veriler (çoğu .NET sayısal türü, sabit listesi, bool, string, DateTime)
  • ve DataMember öznitelikleriyle DataContract işaretlenmiş genişletici tanımlı türler (ve tüm veri üyeleri de serileştirilebilir)
  • IAsyncCommand uygulayan nesneler
  • XamlFragment ve SolidColorBrush nesneleri ve Color değerleri
  • Nullable<> serileştirilebilir bir tür için değerler
  • gözlemlenebilir koleksiyonlar da dahil olmak üzere serileştirilebilir tür koleksiyonları.

Uzak Kullanıcı Denetiminin Yaşam Döngüsü

Denetim bir WPF kapsayıcısına ControlLoadedAsync ilk kez yüklendiğinde bildirim almak için yöntemini geçersiz kılabilirsiniz. Uygulamanızda veri bağlamının durumu UI olaylarından bağımsız olarak değişebilirse, ControlLoadedAsync yöntem veri bağlamının içeriğini başlatmak ve buna değişiklik uygulamaya başlamak için doğru yerdir.

Ayrıca, denetim yok edildiğinde ve artık kullanılmadığında bildirim almak için yöntemini geçersiz kılabilirsiniz Dispose .

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

Komutlar, gözlemlenebilirlik ve iki yönlü veri bağlama

Şimdi veri bağlamını gözlemlenebilir hale getirelim ve araç kutusuna bir düğme ekleyelim.

INotifyPropertyChanged uygulanarak veri bağlamı gözlemlenebilir hale getirilebilir. Alternatif olarak, Uzak kullanıcı arabirimi ortak kodu azaltmak için genişletebileceğimiz kullanışlı bir soyut sınıf NotifyPropertyChangedObjectsağlar.

Veri bağlamı genellikle salt okunur özelliklerin ve gözlemlenebilir özelliklerin bir karışımına sahiptir. Veri bağlamı, ve DataMember öznitelikleriyle DataContract işaretlendikleri ve gerektiğinde INotifyPropertyChanged uygulandığı sürece nesnelerin karmaşık bir grafiği olabilir. Gözlemlenebilir koleksiyonlar veya uzak kullanıcı arabirimi tarafından sağlanan genişletilmiş bir ObservableCollection<T olan ve daha iyi performans sağlayan aralık işlemlerini desteklemek için observableList<T>> de olabilir.

Ayrıca veri bağlamı için bir komut eklememiz gerekir. Uzak kullanıcı arabiriminde komutlar uygulanır IAsyncCommand , ancak sınıfın bir örneğini AsyncCommand oluşturmak genellikle daha kolaydır.

IAsyncCommandICommand iki şekilde farklıdır:

  • Execute Uzak kullanıcı arabirimindeki her şey zaman uyumsuz olduğundan yöntemi ile ExecuteAsync değiştirilir!
  • CanExecute(object) yöntemi bir CanExecute özellik ile değiştirilir. AsyncCommand Sınıf gözlemlenebilir hale getirmekle CanExecute ilgilenir.

Uzak kullanıcı arabiriminin olay işleyicilerini desteklemediğini, bu nedenle kullanıcı arabiriminden uzantıya tüm bildirimlerin veri bağlama ve komutlar aracılığıyla uygulanması gerektiğini unutmayın.

Bu, için MyToolWindowDatasonuçta elde edilen koddur:

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

Oluşturucuyu düzeltin MyToolWindowContent :

public MyToolWindowContent()
    : base(dataContext: new MyToolWindowData())
{
}

Veri bağlamındaki yeni özellikleri kullanacak şekilde güncelleştirin MyToolWindowContent.xaml . Bunların tümü normal WPF XAML'dir. Nesneye IAsyncCommand bile Visual Studio işleminde çağrılan ICommand bir ara sunucu üzerinden erişilir, böylece her zamanki gibi verilere bağlanabilir.

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

İki yönlü bağlama ve komut içeren araç penceresinin diyagramı.

Uzak kullanıcı arabiriminde zaman uyumsuzluğu anlama

Bu araç penceresi için uzak kullanıcı arabirimi iletişiminin tamamı şu adımları izler:

  1. Veri bağlamı, özgün içeriğiyle Visual Studio işleminin içindeki bir ara sunucu aracılığıyla erişilir.

  2. 'den MyToolWindowContent.xaml oluşturulan denetim, veri bağlamı ara sunucusuna bağlı olan verilerdir.

  3. Kullanıcı, veri bağlama aracılığıyla veri bağlamı ara sunucusunun özelliğine Name atanan metin kutusuna bazı metinler yazın. yeni değeri Name nesnesine MyToolWindowData yayılır.

  4. Kullanıcı düğmeye tıklar ve art arda efektlere neden olur:

    • HelloCommand veri bağlamı ara sunucusu yürütülür
    • genişletici AsyncCommand kodunun zaman uyumsuz yürütülmesi başlatılır
    • için HelloCommand zaman uyumsuz geri arama, gözlemlenebilir özelliğin değerini güncelleştirir Text
    • yeni değeri Text veri bağlamı ara sunucusuna yayılır
    • araç penceresindeki metin bloğu, veri bağlama aracılığıyla yeni değerine Text güncelleştirilir

Araç penceresi iki yönlü bağlama ve komut iletişimi diyagramı.

Yarış koşullarından kaçınmak için komut parametrelerini kullanma

Visual Studio ile uzantı arasındaki iletişimi içeren tüm işlemler (diyagramdaki mavi oklar) zaman uyumsuz olur. Uzantının genel tasarımında bu yönü göz önünde bulundurmak önemlidir.

Bu nedenle tutarlılık önemliyse, bir komutun yürütülmesi sırasında veri bağlamı durumunu almak için iki yönlü bağlama yerine komut parametrelerini kullanmak daha iyidir.

Düğmelerini CommandParameterNameile bağlayarak bu değişikliği yapın:

<Button Content="Say Hello" Command="{Binding HelloCommand}" CommandParameter="{Binding Name}" Grid.Column="2" />

Ardından komutunun geri çağırmasını parametresini kullanacak şekilde değiştirin:

HelloCommand = new AsyncCommand((parameter, cancellationToken) =>
{
    Text = $"Hello {(string)parameter!}!";
    return Task.CompletedTask;
});

Bu yaklaşımla, özelliğin Name değeri düğme tıklandığında veri bağlamı ara sunucusundan zaman uyumlu olarak alınır ve uzantıya gönderilir. Bu, özellikle gelecekte geri çağırmanın verim (ifadelere sahipawait) olarak değiştirilmesi durumunda HelloCommand herhangi bir yarış koşulunu önler.

Zaman uyumsuz komutlar birden çok özellikten veri tüketir

Komutun kullanıcı tarafından ayarlanan birden çok özelliği tüketmesi gerekiyorsa komut parametresi kullanmak bir seçenek değildir. Örneğin, kullanıcı arabiriminde iki metin kutusu varsa: "Ad" ve "Soyadı".

Bu durumda çözüm, zaman uyumsuz komut geri çağırmasında veri bağlamındaki tüm özelliklerin değerini döndürmeden önce almaktır.

Aşağıda, komut çağırma sırasındaki değerin FirstName kullanıldığından emin olmak için vermeden önce ve LastName özellik değerlerinin alındığı bir örnek görebilirsiniz:

HelloCommand = new(async (parameter, cancellationToken) =>
{
    string firstName = FirstName;
    string lastName = LastName;
    await Task.Delay(TimeSpan.FromSeconds(1));
    Text = $"Hello {firstName} {lastName}!";
});

Uzantının, kullanıcı tarafından da güncelleştirilebilecek özelliklerin değerini zaman uyumsuz olarak güncelleştirmesini önlemek de önemlidir. Başka bir deyişle, TwoWay veri bağlamasından kaçının.

Buradaki bilgiler basit Uzak Kullanıcı Arabirimi bileşenleri oluşturmak için yeterli olmalıdır. Daha gelişmiş senaryolar için bkz . Gelişmiş Uzak Kullanıcı Arabirimi kavramları.