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