INotifyPropertyChange'i uygulama kolay yoldan
Ö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 INotifyPropertyChanged
ile 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ı ObservableObject
yeni 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ı ObservableObject
yeni 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.
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 INotifyPropertyChanged
ilgili 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 ObservableObject
devralan 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 true
olay 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 ObservableObject
devralan bir sınıfa taşıdık.
adlı MainPageLogic
yeni 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 ObservableObject
devralması 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 MainPageLogic
taşı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ı Logic
MainPage
bir özellik oluşturacağız. Bu türünde MainPageLogic
olacak 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 MainPageLogic
taşı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 Submit
olarak 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.RaisePropertyChanged
değ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 Submit
MainPageLogic
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 INotifyPropertyChanged
MainPage
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ı Clock
INotifyPropertyChanged
değ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 ObservableObject
devralması 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.