Aracılığıyla paylaş


Öğretici: Gelişmiş uzak kullanıcı arabirimi

Bu öğreticide, rastgele renklerin listesini gösteren bir araç penceresini artımlı olarak değiştirerek gelişmiş Uzak kullanıcı arabirimi kavramları hakkında bilgi ediniyorsunuz:

Rastgele renkler araç penceresini gösteren ekran görüntüsü.

Şu konularda bilgi edineceksiniz:

  • Birden çok zaman uyumsuz komut yürütmenin paralel olarak nasıl çalışabileceği ve bir komut çalışırken kullanıcı arabirimi öğelerini devre dışı bırakma.
  • Birden çok düğmeyi aynı zaman uyumsuz komuta bağlama.
  • Başvuru türlerinin Uzak UI veri bağlamında ve proxy'sinde nasıl işleneceğini.
  • Zaman uyumsuz bir komutu olay işleyicisi olarak kullanma.
  • Birden çok düğme aynı komuta bağlıysa, zaman uyumsuz komutunun geri çağırması yürütülürken tek bir düğmeyi devre dışı bırakma.
  • Uzak kullanıcı arabirimi denetiminden XAML kaynak sözlüklerini kullanma.
  • Uzak UI veri bağlamında karmaşık fırçalar gibi WPF türlerini kullanma.
  • Uzak kullanıcı arabiriminin iş parçacığını işleme şekli.

Bu öğretici, giriş niteliğindeki Uzak Kullanıcı Arabirimi makalesini temel alır ve aşağıdakiler dahil olmak üzere çalışan bir VisualStudio.Genişletilebilirlik uzantısına sahip olduğunuzu bekler:

  1. araç penceresini açan komut için bir .cs dosya,
  2. sınıfı için ToolWindow bir MyToolWindow.cs dosya,
  3. sınıfı için RemoteUserControl bir MyToolWindowContent.cs dosya,
  4. xaml tanımı için RemoteUserControl eklenmiş bir MyToolWindowContent.xaml kaynak dosyası,
  5. veri bağlamı RemoteUserControliçin bir MyToolWindowData.cs dosya.

Başlamak için liste görünümünü ve düğmeyi gösterecek şekilde güncelleştirin 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"
              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 x:Name="RootGrid">
        <Grid.Resources>
            <Style TargetType="ListView" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogListViewStyleKey}}" />
            <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.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListView ItemsSource="{Binding Colors}" HorizontalContentAlignment="Stretch">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>
                        <TextBlock Text="{Binding ColorText}" />
                        <Rectangle Fill="{Binding Color}" Width="50px" Grid.Column="1" />
                        <Button Content="Remove" Grid.Column="2" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Button Content="Add color" Command="{Binding AddColorCommand}" Grid.Row="1" />
    </Grid>
</DataTemplate>

Ardından veri bağlamı sınıfını MyToolWindowData.csgüncelleştirin:

using Microsoft.VisualStudio.Extensibility.UI;
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
using System.Text;
using System.Windows.Media;

namespace MyToolWindowExtension;

[DataContract]
internal class MyToolWindowData
{
    private Random random = new();

    public MyToolWindowData()
    {
        AddColorCommand = new AsyncCommand(async (parameter, cancellationToken) =>
        {
            await Task.Delay(TimeSpan.FromSeconds(2));

            var color = new byte[3];
            random.NextBytes(color);
            Colors.Add(new MyColor(color[0], color[1], color[2]));
        });
    }

    [DataMember]
    public ObservableList<MyColor> Colors { get; } = new();

    [DataMember]
    public AsyncCommand AddColorCommand { get; }

    [DataContract]
    public class MyColor
    {
        public MyColor(byte r, byte g, byte b)
        {
            ColorText = Color = $"#{r:X2}{g:X2}{b:X2}";
        }

        [DataMember]
        public string ColorText { get; }

        [DataMember]
        public string Color { get; }
    }
}

Bu kodda yalnızca birkaç önemli şey vardır:

  • MyColor.Colorstring, ancak XAML'de verilere bağlı olduğunda olarak Brush kullanılır, bu WPF tarafından sağlanan bir özelliktir.
  • Zaman AddColorCommand uyumsuz geri çağırma, uzun süre çalışan bir işlemin benzetimini yapmak için 2 saniyelik bir gecikme içerir.
  • Uzak kullanıcı arabirimi tarafından sağlanan genişletilmiş bir ObservableCollection T olan ObservableList<T'yi>>, daha iyi performans sağlayan aralık işlemlerini de desteklemek için kullanırız.<
  • MyToolWindowDatave MyColor şu anda tüm özellikler salt okunur olduğundan INotifyPropertyChanged uygulamayın.

Uzun süre çalışan zaman uyumsuz komutları işleme

Uzak kullanıcı arabirimi ile normal WPF arasındaki en önemli farklardan biri, kullanıcı arabirimi ile uzantı arasındaki iletişimi içeren tüm işlemlerin zaman uyumsuz olmasıdır.

Gibi AddColorCommand zaman uyumsuz komutlar, zaman uyumsuz bir geri çağırma sağlayarak bunu açık hale getirir.

Renk ekle düğmesine kısa bir süre içinde birden çok kez tıklarsanız bunun etkisini görebilirsiniz: Her komut yürütmesi 2 saniye sürdüğünden, birden çok yürütme paralel olarak gerçekleşir ve 2 saniyelik gecikme sona erdiğinde birden çok renk birlikte listede görünür. Bu, kullanıcıya Renk ekle düğmesinin çalışmadığını gösterebilir.

Çakışan zaman uyumsuz komut yürütme diyagramı.

Bu sorunu gidermek için, zaman uyumsuz komut yürütülürken düğmeyi devre dışı bırakın. Bunu yapmak için en basit yol, komutu false olarak ayarlamaktır CanExecute :

AddColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
    AddColorCommand!.CanExecute = false;
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(2));
        var color = new byte[3];
        random.NextBytes(color);
        Colors.Add(new MyColor(color[0], color[1], color[2]));
    }
    finally
    {
        AddColorCommand.CanExecute = true;
    }
});

Kullanıcı düğmeye tıkladığında, geri çağırma komutu uzantıda zaman uyumsuz olarak yürütülür, geri arama olarak ayarlanır CanExecutefalseve ardından Visual Studio işlemindeki ara sunucu veri bağlamı için zaman uyumsuz olarak yayılır ve düğme devre dışı bırakılır. Kullanıcı, düğme devre dışı bırakılmadan önce düğmeye iki kez tıklayabilir.

Daha iyi bir çözüm, zaman uyumsuz komutların RunningCommandsCountözelliğini kullanmaktır:

<Button Content="Add color" Command="{Binding AddColorCommand}" IsEnabled="{Binding AddColorCommand.RunningCommandsCount.IsZero}" Grid.Row="1" />

RunningCommandsCount , komutun şu anda kaç eşzamanlı zaman uyumsuz yürütmesinin devam etmekte olduğunu gösteren bir sayaçtır. Bu sayaç, düğmesine tıklandığında kullanıcı arabirimi iş parçacığında artırılır ve bu da öğesine bağlanarak IsEnabled düğmeyi zaman uyumlu bir şekilde devre dışı bırakmanızı RunningCommandsCount.IsZerosağlar.

Tüm Uzak UI komutları zaman uyumsuz olarak yürütülürken en iyi yöntem, komutun hızla tamamlanması beklense bile uygun olduğunda denetimleri devre dışı bırakmak için kullanmaktır RunningCommandsCount.IsZero .

Zaman uyumsuz komutlar ve veri şablonları

Bu bölümde, kullanıcının listeden bir girişi silmesini sağlayan Kaldır düğmesini uygulayacaksınız. Her MyColor nesne için tek bir zaman uyumsuz komut oluşturabilir veya içinde MyToolWindowData tek bir zaman uyumsuz komutumuz olabilir ve hangi rengin kaldırılması gerektiğini belirlemek için bir parametre kullanabiliriz. İkinci seçenek daha temiz bir tasarımdır, bu nedenle bunu uygulayalım.

  1. Veri şablonunda düğme XAML'sini güncelleştirin:
<Button Content="Remove" Grid.Column="2"
        Command="{Binding DataContext.RemoveColorCommand,
            RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}}}"
        CommandParameter="{Binding}"
        IsEnabled="{Binding DataContext.RemoveColorCommand.RunningCommandsCount.IsZero,
            RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}}}" />
  1. buna karşılık gelen AsyncCommand öğesini MyToolWindowDataekleyin:
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
  1. öğesinin oluşturucusunda komutun zaman uyumsuz geri çağırmasını MyToolWindowDataayarlayın:
RemoveColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
    await Task.Delay(TimeSpan.FromSeconds(2));

    Colors.Remove((MyColor)parameter!);
});

Bu kod, uzun süre çalışan bir zaman uyumsuz komut yürütmenin benzetimini yapmak için kullanırTask.Delay.

Veri bağlamında başvuru türleri

Önceki kodda, bir MyColor nesne zaman uyumsuz komutun parametresi olarak alınır ve kaldırılacak öğeyi tanımlamak için başvuru eşitliğini (MyColorgeçersiz kılmayan Equalsbir başvuru türü olduğundan) kullanan bir List<T>.Remove çağrının parametresi olarak kullanılır. Bu mümkündür, çünkü parametre kullanıcı arabiriminden alınsa bile, bunun tam örneği MyColor şu anda veri bağlamının bir parçası olarak alınır, kopya alınmaz.

İşlemleri:

  • uzak kullanıcı denetiminin veri bağlamını ara sunucu olarak kullanma;
  • uzantıdan Visual Studio'ya güncelleştirme gönderme INotifyPropertyChanged veya bunun tersi;
  • uzantıdan Visual Studio'ya gözlemlenebilir koleksiyon güncelleştirmeleri gönderme veya tam tersi;
  • zaman uyumsuz komut parametreleri gönderme

tümü başvuru türü nesnelerinin kimliğine saygı gösterir. Dizeler dışında, uzantıya geri aktarıldığında başvuru türü nesneleri hiçbir zaman çoğaltılır.

Uzak UI veri bağlama başvuru türlerinin diyagramı.

Resimde, veri bağlamındaki her başvuru türü nesnesine (komutlar, koleksiyon, her MyColor biri ve hatta tüm veri bağlamı) Uzak UI altyapısı tarafından benzersiz bir tanımlayıcı atandığı görebilirsiniz. Kullanıcı ara sunucu renk nesnesi #5 için Kaldır düğmesine tıkladığında, nesnenin değeri değil benzersiz tanımlayıcı (#5) uzantıya geri gönderilir. Uzak UI altyapısı, karşılık gelen MyColor nesneyi alıp zaman uyumsuz komutun geri çağırmasına parametre olarak geçirme işlemini üstlenir.

Birden çok bağlama ve olay işleme ile RunningCommandsCount

Uzantıyı bu noktada test ederseniz Kaldır düğmelerinden birine tıklandığında tüm Kaldır düğmelerinin devre dışı bırakıldığını görebilirsiniz:

Birden çok bağlamaya sahip zaman uyumsuz Komut diyagramı.

İstenen davranış bu olabilir. Ancak, yalnızca geçerli düğmenin devre dışı bırakılmasını istediğinizi ve kullanıcının kaldırma için birden çok rengi kuyruğa almasına izin verebileceğinizi varsayalım: Tüm düğmeler arasında paylaşılan tek bir komutumuz olduğundan zaman uyumsuz komutunRunningCommandsCount özelliğini kullanamıyoruz.

Her düğmeye bir RunningCommandsCount özellik ekleyerek hedefimize ulaşabiliriz, böylece her renk için ayrı bir sayacımız olur. Bu özellikler, XAML'den http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml Uzak UI türlerini kullanmanıza olanak tanıyan ad alanı tarafından sağlanır:

Kaldır düğmesini aşağıdaki şekilde değiştiririz:

<Button Content="Remove" Grid.Column="2"
        IsEnabled="{Binding Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero, RelativeSource={RelativeSource Self}}">
    <vs:ExtensibilityUICommands.EventHandlers>
        <vs:EventHandlerCollection>
            <vs:EventHandler Event="Click"
                             Command="{Binding DataContext.RemoveColorCommand, ElementName=RootGrid}"
                             CommandParameter="{Binding}"
                             CounterTarget="{Binding RelativeSource={RelativeSource Self}}" />
        </vs:EventHandlerCollection>
    </vs:ExtensibilityUICommands.EventHandlers>
</Button>

vs:ExtensibilityUICommands.EventHandlers Ekli özellik, herhangi bir olaya (örneğin, MouseRightButtonUp) zaman uyumsuz komutlar atamaya olanak tanır ve daha gelişmiş senaryolarda yararlı olabilir.

vs:EventHandler ayrıca bir CounterTargetözelliğin eklenmesi gereken bir de olabilir: UIElementvs:ExtensibilityUICommands.RunningCommandsCount bu belirli olayla ilgili etkin yürütmeleri sayarak. Ekli bir özelliğe bağlarken parantez (örneğin Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero) kullandığınızdan emin olun.

Bu durumda, her düğmeye ayrı bir etkin komut yürütme sayacı eklemek için kullanırız vs:EventHandler . Ekli özelliğe bağlanıldığında IsEnabled , ilgili renk kaldırıldığında yalnızca belirli bir düğme devre dışı bırakılır:

Hedeflenen RunningCommandsCount ile zaman uyumsuz Komut diyagramı.

Kullanıcı XAML kaynak sözlükleri

Visual Studio 17.10'dan başlayarak, Uzak Kullanıcı Arabirimi XAML kaynak sözlüklerini destekler. Bu, birden çok Uzak Kullanıcı Arabirimi denetiminin stilleri, şablonları ve diğer kaynakları paylaşmasına olanak tanır. Ayrıca, farklı diller için farklı kaynaklar (örn. dizeler) tanımlamanızı sağlar.

Uzak kullanıcı arabirimi denetimi XAML'sine benzer şekilde, kaynak dosyalarının ekli kaynaklar olarak yapılandırılması gerekir:

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

Uzak kullanıcı arabirimi, kaynak sözlüklerine WPF'den farklı bir şekilde başvurur: Bunlar denetimin birleştirilmiş sözlüklerine eklenmez (birleştirilmiş sözlükler Uzak KULLANıCı Arabirimi tarafından hiç desteklenmez) ancak denetimin .cs dosyasındaki adla başvurulur:

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: new MyToolWindowData())
    {
        this.ResourceDictionaries.AddEmbeddedResource(
            "MyToolWindowExtension.MyResources.xaml");
    }
...

AddEmbeddedResource , varsayılan olarak projenin kök ad alanından, altında olabileceği alt klasör yolundan ve dosya adından oluşan ekli kaynağın tam adını alır. Proje dosyasında için bir LogicalNameEmbeddedResource ayarlayarak bu adı geçersiz kılmak mümkündür.

Kaynak dosyasının kendisi normal bir WPF kaynak sözlüğüdür:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:system="clr-namespace:System;assembly=mscorlib">
  <system:String x:Key="removeButtonText">Remove</system:String>
  <system:String x:Key="addButtonText">Add color</system:String>
</ResourceDictionary>

kullanarak DynamicResourceUzak kullanıcı arabirimi denetimindeki kaynak sözlüğünden bir kaynağa başvurabilirsiniz:

<Button Content="{DynamicResource removeButtonText}" ...

XAML kaynak sözlüklerini yerelleştirme

Uzak UI kaynak sözlükleri, ekli kaynakları yerelleştirdiğiniz gibi yerelleştirilebilir: Aynı ada ve dil sonekine sahip diğer XAML dosyalarını oluşturursunuz, örneğin MyResources.it.xaml İtalyanca kaynaklar için:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:system="clr-namespace:System;assembly=mscorlib">
  <system:String x:Key="removeButtonText">Rimuovi</system:String>
  <system:String x:Key="addButtonText">Aggiungi colore</system:String>
</ResourceDictionary>

Proje dosyasında joker karakterler kullanarak tüm yerelleştirilmiş XAML sözlüklerini ekli kaynaklar olarak ekleyebilirsiniz:

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

Veri bağlamında WPF türlerini kullanma

Şimdiye kadar, uzak kullanıcı denetimimizin veri bağlamı ilkel öğelerden (sayılar, dizeler vb.), gözlemlenebilir koleksiyonlardan ve ile DataContractişaretlenmiş kendi sınıflarımızdan oluşuyordu. bazen karmaşık fırçalar gibi veri bağlamında basit WPF türleri eklemek yararlı olabilir.

VisualStudio.Genişletilebilirlik uzantısı Visual Studio işleminde bile çalışmayabileceği için WPF nesnelerini kullanıcı arabirimiyle doğrudan paylaşamaz. Uzantının WPF türlerine erişimi bile olmayabilir çünkü hedefleyebilir netstandard2.0 veya net6.0 (değişkene -windows erişemez).

Uzak kullanıcı arabirimi, uzak kullanıcı denetiminin XamlFragmentveri bağlamında WPF nesnesinin XAML tanımının dahil edilmesini sağlayan türü sağlar:

[DataContract]
public class MyColor
{
    public MyColor(byte r, byte g, byte b)
    {
        ColorText = $"#{r:X2}{g:X2}{b:X2}";
        Color = new(@$"<LinearGradientBrush xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                               StartPoint=""0,0"" EndPoint=""1,1"">
                           <GradientStop Color=""Black"" Offset=""0.0"" />
                           <GradientStop Color=""{ColorText}"" Offset=""0.7"" />
                       </LinearGradientBrush>");
    }

    [DataMember]
    public string ColorText { get; }

    [DataMember]
    public XamlFragment Color { get; }
}

Yukarıdaki kodla, Color özellik değeri veri bağlamı ara sunucusundaki bir LinearGradientBrush nesneye dönüştürülür: Veri bağlamında WPF türlerini gösteren ekran görüntüsü

Uzak kullanıcı arabirimi ve iş parçacıkları

Zaman uyumsuz komut geri çağırmaları (ve INotifyPropertyChanged kullanıcı arabirimi tarafından veri teklifi yoluyla güncelleştirilen değerler için geri çağırmalar) rastgele iş parçacığı havuzu iş parçacıklarında oluşturulur. Geri çağırmalar birer birer oluşturulur ve kod denetimi (ifade await kullanarak) elde edene kadar çakışmaz.

Bu davranış oluşturucuya NonConcurrentSynchronizationContext RemoteUserControl geçirilerek değiştirilebilir. Bu durumda, bu denetimle ilgili tüm zaman uyumsuz komut ve INotifyPropertyChanged geri çağırmalar için sağlanan eşitleme bağlamını kullanabilirsiniz.