WPF dostarcza obiekt Freezable. Możemy dziedziczyć po tej klasie aby stworzyć własne obiekty immutable. Ze względów wydajnościowych, można takowy obiekt “zamrozić” i wtedy nie można już go więcej modyfikować – staje się więc immutable. Przykład:
internal class SampleClass : Freezable
{
public SampleClass(string text)
{
Text = text;
}
protected override Freezable CreateInstanceCore()
{
return new SampleClass(null);
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof (string), typeof (SampleClass));
public string Text
{
get { return this.GetValue(TextProperty) as string; }
set { this.SetValue(TextProperty, value); }
}
}
Przykład nie jest zbyt skomplikowany. Dziedziczymy po Freezable i implementujemy CreateInstanceCore, która będzie służyć do klonowania obiektów ( o tym za chwilę). Aby zamrozić obiekt wystarczy wywołać metodę Freeze:
var sampleClass = new SampleClass("tekst");
sampleClass.Freeze();
sampleClass.Text = "Nie da rady...";
Po zamrożeniu, próba modyfikacji na właściwości Text zakończy się wyjątkiem InvalidOperationException: “Cannot set a property on object ‘WpfApplication2.SampleClass’ because it is in a read-only state.” Freezable sam dba o zabronienie dostępu do właściwości pod warunkiem, że są one typu DependencyProperties. W innym przypadku sprawa wygląda bardziej skomplikowanie. Wiemy, że próba zakończy się wyjątkiem zatem bezpieczniej w kodzie sprawdzać w jakim stanie jest aktualnie obiekt:
if(sampleClass.IsFrozen==false)
sampleClass.Text = "Value";
Obiekt raz zamrożony nie może zostać odblokowany. Jedyna możliwość to sklonowanie jego np:
var sampleClass = new SampleClass("tekst");
if(sampleClass.CanFreeze)
sampleClass.Freeze();
var clonedObject = (SampleClass)sampleClass.Clone();
MessageBox.Show(clonedObject.IsFrozen.ToString());// po sklonowaniu, ponownie false
MessageBox.Show(clonedObject.Text);
Clone wykonuje głęboką kopie obiektu. Zaimplementowana prze nas metoda CreateInstanceCore tworzy jedynie instancję obiektu – wszystkie właściwości zostaną sklonowane za nas automatycznie. CreateInstnaceCore ogranicza się zwykle do wywołania domyślnego konstruktora – reszta zostanie wykonana przez Freezable.
Freezable dostarcza również zdarzenie Changed, przydatne podczas monitorowania stanu obiektu:
private void Button_Click(object sender, RoutedEventArgs e)
{
var sampleClass = new SampleClass("tekst");
sampleClass.Changed += new EventHandler(SampleClassChanged);
sampleClass.Text = "Zmiana tekstu";
}
void SampleClassChanged(object sender, EventArgs e)
{
}
Jakie ma to praktyczne zastosowanie? Klasa Freezable została dodana głównie ze względów wydajnościowych. Zamrożony obiekt nie zmieni swojego stanu zatem nie musimy go obserwować. Na przykład kontrolki w WPF muszą być przerenderowane gdy któryś z obiektów zmienił swój stan. Wspomniane wcześniej zdarzenie Changed nie będzie musiało już być monitorowane. Klasy zamrożone są oczywiste bezpieczne z punktu widzenia wielowątkowości ponieważ mamy pewność, że właściwości nie zostaną zmodyfikowane z innego wątku. Dzięki temu, nie trzeba używać czasochłonnych blokad (lock) lub innych metod synchronizacji.
Freezable jest powszechny w kontrolkach WPF. Na przykład zdarzenie Brush.Changed jest śledzone i w momencie jego wywołania, kontrolka jest przerenderowywana z nowym kolorem. Inne przykłady to klasy Transform , Geometry czy animacje. Wszystkie pędzle systemowe są domyślnie zamrożone. Nie możliwe jest na przykład zmiana koloru dla jakiekolwiek systemowego pędzla:
SolidColorBrush systemBrush = Brushes.White;
MessageBox.Show(systemBrush.IsFrozen.ToString()); // zawsze true
SolidColorBrush customBrush=new SolidColorBrush(Colors.AliceBlue);
MessageBox.Show(customBrush.IsFrozen.ToString()); // domyslnie false
Istnieje również możliwość zamrożenia obiektu prosto z poziomu XAML. Załóżmy, że definiujemy Brush w zasobach:
<LinearGradientBrush ice:Freeze="True"
xmlns:ice="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
/>