Проектирование приложения с учетом использования единого опыта разработки для настольных и Web-проектов
Решение проблем недостающего функционала
Отсутствие FrameworkPropertyMetadata
Как известно, в Silverlight отсутствует класс FrameworkPropertyMetadata. Одна из наиболее часто используемых возможностей этого FrameworkPropertyMetadata по сравнению с имеющемся в Silverlight PropertyMetadata – это логические переключатели, которые управляют влиянием свойства на различные аспекты прорисовки объекта, такие как AffectsMeasure, AffectsArrange, и другие. Если один из флагов установлен в истинное значение, соответствующий аспект объявляется недействительным.
К счастью, данное поведение достаточно легко эмулировать при помощи PropertyMetadata и PropertyChangedCallback. Так, если в случае разработки только для WPF можно написать:
1: public static readonly DependencyProperty SomethingProperty 2: = DependencyProperty.Register( 3: "Something", typeof(string), typeof(Window1), 4: new FrameworkPropertyMetadata(string.Empty, 5: FrameworkPropertyMetadataOptions.AffectsMeasure) 6: );
То универсальный WPF/Silverlight код, достигающий такого же эффекта, будет выглядеть следующим образом:
1: public static readonly DependencyProperty SomethingProperty 2: = DependencyProperty.Register( 3: "Something", typeof(string), typeof(Window1), 4: new PropertyMetadata(string.Empty, 5: new PropertyChangedCallback( 6: Window1.SomethingProperty_Changed))); 7: 8: private static void SomethingProperty_Changed( 9: DependencyObject d, DependencyPropertyChangedEventArgs e) 10: { 11: ((FrameworkElement)d).InvalidateMeasure(); 12: }
Чтобы не допустить копирования кода, имеет смысл создать вспомогательный класс, методы которого устанавливают недействительными различные аспекты и их комбинации, и использовать эти методы в обработчиках изменений зависимых свойств, если нет необходимости добавлять в них дополнительную логику.
Отсутствие приведения значения
WPF имеет достаточно мощный механизм приведения значений зависимых свойств. К сожалению, такого функционала в Silverlight нет. Приведение значений возможно производить при помощи сочетания PropertyChangedCallback и обычных методов класса. В некоторых сложных, но не слишком критичных сценариях, возможно, имеет смысл использовать приведение значения в WPF версии и не проверять его совсем в Silverlight версии.
Отсутствие метода OverrideMetadata()
WPF позволяет переопределять метаданные зависимых свойств при помощи метода OverrideMetadata объекта DependencyProperty. В следующем коде переопределяется ширина по умолчанию у элемента управления, наследованного от класса Control:
1: static MyControl() 2: { 3: FrameworkPropertyMetadata newMetadata 4: = new FrameworkPropertyMetadata(); 5: newMetadata.DefaultValue = 180.0; 6: Control.WidthProperty.OverrideMetadata(typeof(MyControl), 7: newMetadata); 8: }
Так как в Silverlight такой метод отсутствует, при необходимости изменить значение по умолчанию какого-либо свойства необходимо просто установить это значение в конструкторе:
1: public MyControl() 2: { 3: this.Width = 180; 4: }
Другая возможность метода OverrideMetadata() – это задание обработчика изменения зависимого свойства базового класса:
1: static MyControl() 2: { 3: #if !SILVERLIGHT 4: FrameworkPropertyMetadata newMetadata 5: = new FrameworkPropertyMetadata(); 6: newMetadata.PropertyChangedCallback 7: += MyControl.VisibilityProperty_Changed; 8: Control.VisibilityProperty.OverrideMetadata( 9: typeof(MyControl), newMetadata); 10: #endif 11: }
После этого в методе VisibilityProperty_Changed можно обрабатывать все изменения зависимого свойства Visibility.
1: private static void VisibilityProperty_Changed( 2: DependencyObject d, DependencyPropertyChangedEventArgs e) 3: { 4: // do something here 5: }
К сожалению, т.к. данного метода в Silverlight нет совсем, изящного решения данной проблемы не существует. Однако можно воспользоваться не очень чистым приемом, включающим определение дополнительного зависимого свойства и привязку исходного свойства к нему. Аналог приведенного выше примера для Silverlight приложения в таком случае будет выглядеть следующим образом:
1: #if SILVERLIGHT 2: // объявление дополнительного зависимого свойства, которое 3: // использует такой же PropertyChangedCallback, что и WPF код 4: private static readonly DependencyProperty 5: VisibilityChangedWorkaroundProperty 6: = DependencyProperty.Register( 7: "VisibilityChangedWorkaround", typeof(Visibility), 8: typeof(MyControl), 9: new PropertyMetadata(MyControl.VisibilityProperty_Changed)); 10: #endif 11: 12: public MyControl() 13: { 14: #if SILVERLIGHT 15: // привязка дополнительного зависимого свойства 16: Binding visibilityBnd = new Binding("Visibility"); 17: visibilityBnd.Source = this; 18: visibilityBnd.Mode = BindingMode.TwoWay; 19: this.SetBinding( 20: MyControl.VisibilityChangedWorkaroundProperty, 21: visibilityBnd); 22: #endif 23: }
Как видно из примера, определено дополнительное зависимое свойство VisibilityChangedWorkaroundProperty, и в качестве его обработчика изменения указан тот же метод, что и в WPF коде, приведенном выше. Нет необходимости создавать CLR свойство-обертку для дополнительного свойства, и само дополнительное зависимое свойство определено как приватное, так что оно доступно только внутри класса и невидимо для внешних компонент.
Затем в конструкторе дополнительное свойство привязывается к свойству Visibility базового класса. Таким образом, когда свойство базового класса изменится, дополнительное свойство также будет изменено в результате срабатывания привязки, после чего будет вызван обработчик VisibilityProperty_Changed.
Отсутствие зависимых свойств только для чтения
Одна из действительно удобных возможностей WPF – определение зависимых свойств только для чтения. Такое свойство полезно, если необходимо отобразить какой-либо статус объекта, определяемый его внутренним состоянием (как, например, свойство IsMouseOver), и разработчик не хочет позволять внешним компонентам изменять данное значение извне. Такого эффекта можно с легкостью достичь при использовании обычных .NET свойств, но что если необходимо создать свойство только для чтения, которое будет обладать всеми возможностями зависимого свойства?
К сожалению, не существует простого решения для Silverlight. Безусловно, можно создать CLR свойство-обертку для зависимого свойства без метода доступа set, однако при этом любой компонент может установить значение свойству при помощи метода SetValue().
Таким образом, в данном случае приходится выбирать: совсем не использовать зависимые свойства только для чтения или использовать разделение кода (при помощи директив препроцессора или разделяемых классов) между WPF и Silverlight. Следует помнить, что в данном случае придется использовать различный код не только для объявление зависимого свойства, но и в вызове SetValue (т.к. необходимо вызывать SetValue для DependencyPropertyKey в случае зависимого свойства только для чтения). Легко понять, что в таком случае исходный код будет достаточно трудночитаем из-за многочисленных ветвлений препроцессора #if SILVERLIGHT.
Отсутствие класса Brushes
Класс Brushes в WPF содержит предопределенные сплошные кисти, которые достаточно полезны при использовании кистей в коде.
1: border.Background = Brushes.White;
Данный класс отсутствует в Silverlight. При написании WPF/Silverlight совместимого кода следует использовать класс Colors, который достаточно похож на класс Brushes, только вместо предопределенных кистей содержит предопределенные цвета. Таким образом, код выше может быть переписан на:
1: border.Background = new SolidColorBrush(Colors.White);
Конечно, символов стало больше, однако это лучше, чем дублирование кода из-за настолько небольшого отличия.
При использовании именованных кистей в XAML отличий между Silverlight и WPF нет:
1: <Border Name="border" Background="White"> 2: </Border>
Stroke и Pen
В WPF определен класс Pen, в то время как в Silverlight его нет. Класс Pen достаточно удобно инкапсулирует способ рисования контура фигуры в единственном объекте, который возможно повторно использовать. Так, в WPF он применяется в классе GeometryDrawing (которого, кстати, также нет в Silverlight).
Следует отметить, что во многих случаях даже в WPF его невозможно использовать – вместо него приходится применять свойства StrokeXxx. По-видимому, по этой причине в Silverlight этот класс даже не был реализован.