INotifyPropertyChange'i uygulama kolay yoldan

Tamamlandı

Önceki dersleri izlediyseniz, veri bağlamayı uygulamanın çok büyük bir çaba olduğunu düşünebilirsiniz. Neden yalnızca saati görüntülemek için kullanabileceğinizTimeTextBlock.Text = DateTime.Now.ToLongTime(), olayları soldan ve sağdan tetikleme INotifyPropertyChangedile ilgili tüm sorunları yaşıyorsunuz? Bu basit örnekte veri bağlamanın aşırıya kaçması gibi göründüğü doğrudur.

Ancak, veri bağlama çok daha fazlasını yapabilir. Kullanıcı arabirimi ve kod arasında her iki yönde de veri aktarabilir, öğe listelerini görüntüleyebilir ve verilerin düzenlenmesini destekleyebilir. Tüm bunlar, uygulamanızın mantığının üzerinde çalıştığı verilerin ve verilerin sunumunun temiz bir şekilde ayrılmasını sağlayan bir mimariyle.

Ancak geliştiricinin yazması gereken kod miktarını nasıl azaltabiliriz? Kimse bildirmesi gereken her özellik için on kod satırı girmek istemez. Neyse ki ortak işlevselliği ayıklayabilir ve özellik ayarlayıcılarını tek bir kod satırına düşürebiliriz. Bu ders size nasıl yapılacağını gösterir.

Hedef

Amacımız, arabirimi uygulamaya yönelik INotifyPropertyChanged tüm tesisatı ayrı bir sınıfa taşımak ve değiştirildiğinde kullanıcı arabirimini bilgilendirebilecek bir özellik oluşturmayı basitleştirmektir. Hatırlatmak gerekirse, basitleştirmek istediğimiz kod şu şekildedir:

private bool _isNameNeeded = true;

public bool IsNameNeeded
{
    get { return _isNameNeeded; }
    set
    {
        if (value != _isNameNeeded)
        {
            _isNameNeeded = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsNameNeeded)));
        }
    }
}

Ayarlayıcıda bir şey yapmamız gerektiğinden otomatik özellikler (örneğin public bool IsNameNeeded { get; set;}) burada kullanılamaz. Bu nedenle, yedekleme alanı olan özellik bildirim satırı hakkında yapılacak çok fazla şey yoktur. Modern C# özelliklerini kullanarak alıcıyı olarak get => _isNameNeeded;değiştirebiliriz, ancak bu yalnızca birkaç tuş vuruşunu kaydeder. Bu nedenle, dikkatimizi özellik ayarlayıcıya odaklamamız gerekiyor. Bunu tek bir satıra çevirebilir miyiz?

Sınıfı ObservableObject

Yeni bir temel sınıf oluşturabiliriz: ObservableObject. Arabirimi kullanılarak INotifyPropertyChanged kullanıcı arabirimi tarafından gözlemlenebildiği için gözlemlenebilir olarak adlandırılır. Veriler ve mantık, ondan devralınan sınıflarda barındırılır ve kullanıcı arabirimi de bu devralınan sınıfların örneklerine bağlıdır.

1. Sınıfı oluşturma ObservableObject

adlı ObservableObjectyeni bir sınıf oluşturalım. Çözüm Gezgini'de projeye sağ tıklayınDatabindingSample, Ekle / Sınıf'ı seçin ve sınıfın adı olarak girinObservableObject. Sınıfı oluşturmak için Ekle'yi seçin.

1. Sınıfı oluşturma ObservableObject

adlı ObservableObjectyeni bir sınıf oluşturalım. Çözüm Gezgini'da projeye sağ tıklayınDatabindingSampleWPF, Ekle / Sınıf'ı seçin ve sınıfın adı olarak girinObservableObject. Sınıfı oluşturmak için Ekle'yi seçin.

Screenshot of Visual Studio showing the Add New Item dialog with a Visual C# class type selected.

2. Arabirimi uygulama INotifyPropertyChanged

Ardından arabirimini INotifyPropertyChanged uygulamalı ve sınıfımızı genel yapmalıyız. Sınıfın imzasını şöyle görünecek şekilde değiştirin:

public class ObservableObject : INotifyPropertyChanged

Visual Studio ile INotifyPropertyChangedilgili birkaç sorun olduğunu gösterir. Başvurulmayan bir ad alanında bulunur. Burada gösterildiği gibi ekleyelim.

using System.ComponentModel;

Şimdi arabirimini uygulamamız gerekiyor. Bu satırı sınıfın gövdesine ekleyin.

public event PropertyChangedEventHandler? PropertyChanged;

3. RaisePropertyChanged Yöntemi

Önceki derslerde, genellikle kodumuzda özelliğini özellik ayarlayıcıları dışında bile yükseltirdik PropertyChangedEvent . Modern C# ve null koşullu işleç veya (?.) bunu tek satırda yapmamıza izin verirken, aşağıdaki gibi bir kolaylık işlevi oluşturarak da basitleştirebiliriz:

protected void RaisePropertyChanged(string? propertyName)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Bu nedenle, şimdi öğesinden ObservableObjectdevralan sınıflarda, olayı yükseltmek PropertyChanged için yapmamız gereken tek şey şunlardır:

RaisePropertyChanged(nameof(MyProperty));

4. Set<T> Yöntemi

Ancak değerin olduğu gibi aynı olup olmadığını denetleyen, değilse değeri ayarlayan ve olayı yükselten ayarlayıcı deseni PropertyChanged hakkında ne yapabiliriz? İdeal olarak, bunu aşağıdaki gibi tek satırlı bir satıra dönüştürmek istiyoruz:

private bool _isNameNeeded = true;

public bool IsNameNeeded
{
    get { return _isNameNeeded; }
    set { Set(ref _isNameNeeded, value); }  // Just one line!
}

Bundan daha basit olamaz. Bir işlevi çağırır, özelliğin yedekleme alanına bir başvuru geçirir ve yeni değeri ayarlarız. Peki, bu Set yöntem nasıl görünüyor?

protected bool Set<T>(
    ref T field,
    T newValue,
    [CallerMemberName] string? propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(field, newValue))
    {
        return false;
    }

    field = newValue;
    RaisePropertyChanged(propertyName);
    return true;
}

Yukarıdaki kodu sınıfın gövdesine ObservableObject kopyalayın. için [CallerMemberName], dosyanın en üstüne aşağıdaki satırı da eklemeniz gerekir:

using System.Runtime.CompilerServices;

Burada birçok gelişmiş C# ve derleyici sihri var. Şimdi bunlara daha yakından bakalım.

Set<T> genel bir yöntemdir ve derleyicinin yedekleme alanı ile değerin aynı türde olduğundan emin olmasına yardımcı olur. Yöntemin üçüncü parametresi olan propertyName, özniteliği tarafından [CallerMemberName] dekore edilmiştir. yöntemini çağırırken öğesini tanımlamazsak propertyName , çağıran üyenin adını alır ve derleme zamanında buraya yerleştirir. Bu nedenle, yönteminin ayarlayıcısından çağırırsakSet, derleyici "IsNameNeeded" dize sabit değerini üçüncü parametre olarak IsNameNeeded yerleştirir. Dizeleri sabit kodlamaya ve hatta kullanmaya nameof()gerek yok!

Ardından yöntemi, Set alanın geçerli ve yeni değerini karşılaştırmak için çağırır EqualityComparer<T>.Default.Equals . Eski ve yeni değerler eşitse, Set yöntemi döndürür false. Aksi takdirde, yedekleme alanı yeni değere ayarlanır ve PropertyChanged döndürmeden önce trueolay oluşturulur. Değerin değişip değişmediğini belirlemek için yönteminin dönüş Set değerini kullanabilirsiniz.

Sınıfın uygulanmasıyla ObservableObject birlikte, bunu uygulamamızda nasıl kullanabileceğimize bakalım!

5. Sınıfı oluşturma MainPageLogic

Bu dersin başlarında tüm verilerimizi ve mantığımızı sınıfından MainPage ve öğesinden ObservableObjectdevralan bir sınıfa taşıdık.

adlı MainPageLogicyeni bir sınıf oluşturalım. Çözüm Gezgini'de projeye sağ tıklayınDatabindingSample, Ekle / Sınıf'ı seçin ve sınıfın adı olarak girinMainPageLogic. Sınıfı oluşturmak için Ekle'yi seçin.

Sınıfın imzasını değiştirerek ortak olması ve öğesinden ObservableObjectdevralması gerekir.

public class MainPageLogic : ObservableObject
{
}

6. Saat özelliğini sınıfına MainPageLogic taşıma

Saat özelliğinin kodu üç bölümden oluşur: _timer alan, oluşturucuda öğesini ayarlama DispatcherTimer ve CurrentTime özelliği. İkinci derste bıraktığımız kod şu şekildedir:

private DispatcherTimer _timer;

public MainPage()
{
    this.InitializeComponent();
    _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

    _timer.Tick += (sender, o) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));

    _timer.Start();
}

public string CurrentTime => DateTime.Now.ToLongTimeString();

ile _timer ilgili tüm kodu sınıfına MainPageLogic taşıyalım. Oluşturucudaki satırlar (çağrı dışında this.InitializeComponent() ) 'nin oluşturucusna MainPageLogictaşınmalıdır. Yukarıdaki koddan, içinde MainPage bırakılması gereken tek şey oluşturucudaki çağrıdır InitializeComponent .

public MainPage()
{
    this.InitializeComponent();
}

Şimdilik kodun yalnızca bu bölümüne dokunun. Sınıfın kodunun geri kalanına MainPage geri döneceğiz.

Taşıma MainPageLogic sonrasında sınıf şöyle görünür:

public class MainPageLogic : ObservableObject
{
    private DispatcherTimer _timer;

    public MainPageLogic()
    {
        _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

        _timer.Tick += (sender, o) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));

        _timer.Start();
    }

    public string CurrentTime => DateTime.Now.ToLongTimeString();
}

Unutmayın, olayı yükseltmek PropertyChanged için kullanışlı bir işlevimiz var. Bunu işleyicide _timer.Tick kullanalım.

_timer.Tick += (sender, o) => RaisePropertyChanged(nameof(CurrentTime));

7. XAML'yi MainPageLogic

Projeyi şimdi derlemeye çalışırsanız, MainPage.xaml dosyasında "'CurrentTime' Özelliği 'MainPage' türünde bulunamadı" hatasını alırsınız. Ve kesinlikle, sınıfın MainPage artık bir CurrentTime özelliği yoktur. Sınıfa MainPageLogic taşındı. Bunu düzeltmek için sınıfında adlı LogicMainPage bir özellik oluşturacağız. Bu türünde MainPageLogicolacak ve tüm bağlamalarımızı bu şekilde yapacağız.

Sınıfına MainPage aşağıdakileri ekleyin:

public MainPageLogic Logic { get; } = new MainPageLogic();

Ardından MainPage.xaml dosyasında saati görüntüleyen öğesini bulun TextBlock .

<TextBlock Text="{x:Bind CurrentTime, Mode=OneWay}"
           HorizontalAlignment="Right"
           Margin="10"/>

Bağlamaya ekleyerek Logic. bağlamayı değiştirin.

<TextBlock Text="{x:Bind Logic.CurrentTime, Mode=OneWay}"
           HorizontalAlignment="Right"
           Margin="10"/>

Şimdi uygulama derleniyor ve çalıştırırsanız, saat gerektiği gibi işliyor. Güzel!

8. Mantığın geri kalanını taşıma

Hadi hızı artıralım. sınıfındaki kodun MainPage geri kalanını öğesine MainPageLogictaşıyın. Tek kalan özellik, oluşturucu ve PropertyChanged olaydırLogic.

9. Basitleştirme IsNameNeeded

MainPageLogic.cs dosyasında, özellik ayarlayıcısını IsNameNeeded yeni Set yöntemimize yapılan bir çağrıyla değiştirin.

public bool IsNameNeeded
{
    get { return _isNameNeeded; }
    set { Set(ref _isNameNeeded, value); }
}

10. Yöntemini düzeltin OnSubmitClicked

Mantıksal düzeyde, artık düğme tıklama olayının göndereni veya olay birleştirmeleri bizim için önemli değildir. Yöntemin adını yeniden gözden geçirmek de iyi bir uygulamadır. Artık düğme tıklamaları yapmayız, mantık göndeririz. Bu nedenle yöntemini Submitolarak yeniden adlandıralımOnSubmitClicked, bunu genel hale getirelim ve parametreleri kaldıralım.

yönteminin içinde olayı oluşturmanın PropertyChanged eski bir yolu vardır. öğesini çağrısıyla ObservableObject.RaisePropertyChangeddeğiştirin. Sonunda, yöntemin tamamı şu şekilde görünmelidir:

public void Submit()
{
    if (string.IsNullOrEmpty(UserName))
    {
        return;
    }

    IsNameNeeded = false;
    RaisePropertyChanged(nameof(GetGreetingVisibility));
}

11. XAML'yi Logic

Ardından MainPage.xaml'e dönün ve kalan bağlamaları özelliğinden Logic geçecek şekilde değiştirin. Her şey tamamlandığında, Grid aşağıdaki gibi görünmelidir:

<Grid>
    <TextBlock Text="{x:Bind Logic.CurrentTime, Mode=OneWay}"
               HorizontalAlignment="Right"
               Margin="10"/>

    <StackPanel HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Orientation="Horizontal"
                Visibility="{x:Bind Logic.IsNameNeeded, Mode=OneWay}">
        <TextBlock Margin="10"
                   VerticalAlignment="Center"
                   Text="Enter your name: "/>
        <TextBox Name="tbUserName"
                 Margin="10"
                 Width="150"
                 VerticalAlignment="Center"
                 Text="{x:Bind Logic.UserName, Mode=TwoWay}"/>
        <Button Margin="10"
                VerticalAlignment="Center"
                Click="{x:Bind Logic.Submit}" >Submit</Button>
    </StackPanel>

    <TextBlock Text="{x:Bind sys:String.Format('Hello {0}!',  tbUserName.Text), Mode=OneWay}"
               Visibility="{x:Bind Logic.GetGreetingVisibility(), Mode=OneWay}"
               HorizontalAlignment="Left"
               VerticalAlignment="Top"
               Margin="10"/>
</Grid>

Olayın bile Button.Click sınıfındaki yöntemine SubmitMainPageLogic nasıl bağlanabileceğini unutmayın.

Projeyi şimdi derlerseniz, hala hiçbir zaman kullanılmadığını MainPage.PropertyChanged belirten bir uyarı alırsınız.

12. Sınıfı toparlayın MainPage

Uyarı, artık sınıfında arabirime INotifyPropertyChangedMainPage ihtiyaç duymadığımız için oluşur. Bu nedenle, olayıyla PropertyChanged birlikte sınıf bildiriminden kaldıralım.

Sonunda sınıfın tamamı MainPage şöyle görünür:

public sealed partial class MainPage : Page
{
    public MainPageLogic Logic { get; } = new MainPageLogic();

    public MainPage()
    {
        this.InitializeComponent();
    }

}

Bu olabildiğince temiz.

13. Uygulamayı çalıştırma

Her şey yolunda gittiyse, uygulamayı bu noktada çalıştırabilmeniz ve daha önce olduğu gibi çalıştığını doğrulayabilmeniz gerekir. Tebrikler!

Özet

Peki bu kadar çalışmayla ne elde ettik? Uygulama öncekiyle aynı şekilde çalışsa da ölçeklenebilir, sürdürülebilir ve test edilebilir bir mimariye ulaştık.

Sınıf MainPage artık çok basit. Mantığa bir başvuru içerir ve yalnızca bir düğme tıklama olayı alır ve iletir. Mantık ve kullanıcı arabirimi arasındaki tüm veri akışı, hızlı, sağlam ve kanıtlanmış veri bağlama yoluyla gerçekleşir.

Sınıfı MainPageLogic artık ui-agnostic... Saatin bir denetimde mi yoksa başka bir TextBlock denetimde mi görüntülendiği önemli değildir. Form gönderimi çeşitli şekillerde gerçekleşebilir. Bu yöntemler arasında düğme tıklaması, Enter tuşuna basma veya gülümsemeyi algılayan yüz tanıma algoritması bulunur. Form, mantığı hedefleyen ve projenin gereksinimlerine göre çalıştığından emin olan otomatik birim testleri kullanılarak da gönderilebilir.

Bu nedenlerle, diğerlerinde olduğu gibi, sayfanın kodunda yalnızca kullanıcı arabirimiyle ilgili özelliklere sahip olmak ve mantığı farklı bir sınıfta ayırmak iyi bir uygulamadır. Daha karmaşık uygulamalar animasyon denetimine ve diğer somut kullanıcı arabirimi özelliklerine de sahip olabilir. Daha karmaşık uygulamalarla çalışırken, bu derste oluşturduğumuz kullanıcı arabirimi ve mantık ayrımını takdir edersiniz.

Sınıfı kendi projenizde yeniden kullanabilirsiniz ObservableObject . Biraz pratik yaptıktan sonra, sorunlara bu şekilde yaklaşmanın aslında daha hızlı ve daha kolay olduğunu göreceksiniz. Ya da bu modülde öğrendiğiniz ilkeleri izleyen ve temel alan MVVM Araç Seti gibi mevcut, iyi kurulmuş bir kitaplıkta da yararlanabilirsiniz.

5. sınıfından Clock yararlanacak şekilde değiştirin ObservableObject

yerine öğesinden ObservableObject devralması için imzasını ClockINotifyPropertyChangeddeğiştirin.

public class Clock : ObservableObject

Şimdi hem sınıfında hem de Clock temel sınıfında tanımlanan olayımız var PropertyChanged ve bu da derleyici uyarısına neden olur. sınıfından PropertyChanged olayı Clock silin.

Olayı yükseltmek PropertyChanged için sınıfında bir kolaylık işlevi oluşturduk ObservableObject . Bunu kullanmak için satırı şununla _timer.Tick değiştirin:

_timer.Tick += (sender, o) => RaisePropertyChanged(nameof(CurrentTime));

Sınıf Clock zaten daha basit hale geldi. Ama şimdi daha karmaşık MainWindowDataContext sınıfla neler yapabileceğimize bakalım.

6. sınıfından MainWindowDataContext yararlanmak için değiştirin ObservableObject

Clock sınıfında olduğu gibi, sınıf bildirimini öğesinden ObservableObjectdevralması için yeniden değiştirerek başlayacağız.

public class MainWindowDataContext : ObservableObject

Burada da etkinliği sildiğinizden PropertyChanged emin olun.

Özelliğin ayarlayıcısına IsNameNeeded göz atın. Şu anda göründüğü gibi:

set
{
    if (value != _isNameNeeded)
    {
        _isNameNeeded = value;
        PropertyChanged?.Invoke(
            this, new PropertyChangedEventArgs(nameof(IsNameNeeded)));
        PropertyChanged?.Invoke(
            this, new PropertyChangedEventArgs(nameof(GreetingVisibility)));
    }
}

Bu, yeni IsNameNeeded özellik değeri farklıysa ek PropertyChanged olay çağrısı içeren standart INotifyPropertyChanged desendir.

Bu tam olarak işlevin ObservableObject.Set oluşturulduğu durumdur. İşlev, özelliğin Set eski ve yeni değerlerinin farklı olup olmadığını belirten bir bool değer bile döndürür. Bu nedenle, yukarıdaki özellik ayarlayıcısı şu şekilde basitleştirilebilir:

if (Set(ref _isNameNeeded, value))
{
    RaisePropertyChanged(nameof(GreetingVisibility));
}

Fena değil!

7. Uygulamayı çalıştırma

Her şey yolunda gittiyse, uygulamayı bu noktada çalıştırabilmeniz ve daha önce olduğu gibi çalıştığını doğrulayabilmeniz gerekir. Tebrikler!

Özet

Peki bu kadar çalışmayla ne elde ettik? Uygulama öncekiyle aynı şekilde çalışsa da ölçeklenebilir, sürdürülebilir ve test edilebilir bir mimariye ulaştık.

Sınıf MainWindow çok basit. Mantığa bir başvuru içerir ve yalnızca bir düğme tıklama olayı alır ve iletir. Mantık ve kullanıcı arabirimi arasındaki tüm veri akışı, hızlı, sağlam ve kanıtlanmış veri bağlama yoluyla gerçekleşir.

Sınıfı MainWindowDataContext artık ui-agnostic... Saatin bir denetimde mi yoksa başka bir TextBlock denetimde mi görüntülendiği önemli değildir. Form gönderimi çeşitli şekillerde gerçekleşebilir. Bu yöntemler arasında düğme tıklaması, Enter tuşuna basma veya gülümsemeyi algılayan yüz tanıma algoritması bulunur. Form, mantığı hedefleyen ve projenin gereksinimlerine göre çalıştığından emin olan otomatik birim testleri kullanılarak da gönderilebilir.

Bu nedenlerle, diğerlerinin yanı sıra pencerenin arka planında yalnızca ui ile ilgili özelliklere sahip olmak ve mantığı farklı bir sınıfta ayırmak iyi bir uygulamadır. Daha karmaşık uygulamalar animasyon denetimine ve diğer somut kullanıcı arabirimi özelliklerine de sahip olabilir. Daha karmaşık uygulamalarla çalışırken, bu derste oluşturduğumuz kullanıcı arabirimi ve mantık ayrımını takdir edersiniz.

Sınıfı kendi projenizde yeniden kullanabilirsiniz ObservableObject . Biraz pratik yaptıktan sonra, sorunlara bu şekilde yaklaşmanın aslında daha hızlı ve daha kolay olduğunu göreceksiniz. Ya da bu modülde öğrendiğiniz ilkeleri izleyen ve temel alan MVVM Araç Seti gibi mevcut, iyi kurulmuş bir kitaplıkta da yararlanabilirsiniz.