Silverlight 和 WPF 的包装器模式





5.00/5 (9投票s)
一种设计模式,可轻松绑定或为元素中不存在的属性设置动画,并且该模式同时适用于 Silverlight 和 WPF。
目录
引言
距离我上一篇文章已经很久了。这篇文章很短,而且不像我以前的文章那样有创意,但我希望你会喜欢! :)
我希望你们没有人曾经特别谈论过这一点,但你们可能已经使用过我今天要讲的内容,只是没有命名它:WPF 和 Silverlight 中的包装器模式。
模式表
模式类型:表示模式。
问题:你想为一个不存在于对象(目标对象)上的属性创建动画或绑定,或者该属性不支持 Storyboard 或 Binding。
解决方案:创建一个对象(包装器),它将公开该属性,并相应地修改目标对象。
替代方案:
- 使用附加属性;但是,这在 Silverlight 3.0 中会失败(它在 XAML 中不支持对附加属性进行动画处理)。
- 使用装饰器,但 Silverlight 中不存在装饰器,而且有时会使你的 XAML 更难阅读。
Actor:
- 我们想要设置动画的对象称为“
target
”。 - 我们将创建的用于公开新属性以进行动画处理的对象称为“
wrapper
”。
具体示例
- 第一个是 Silverlight 中的一个常见问题:绑定到
ActualWidth
/Height
属性。 - 第二个是一个元素隐藏器;它允许你显示或“推”一个元素到屏幕边缘(就像 Visual Studio 中的可停靠视图一样)。
对于这两个示例,包装器模式提供了一个非常好的解决方案。
示例 1 - 绑定到 ActualWidth 和 ActualHeight
这是示例
滑块绑定到包装绿色矩形的网格的 Height
和 Width
。红色矩形的 width
和 height
绑定到绿色矩形的 ActualWidth
和 ActualHeight
。green
矩形的 Height
和 Width
属性未设置。
这是代码
<StackPanel>
<Canvas Background="Yellow" Height="500" Width="500" >
<Grid x:Name="grid" Canvas.Top="0" Height="100" Width="100">
<Rectangle x:Name="greenRect" Fill="Green"></Rectangle>
</Grid>
<Rectangle Canvas.Top="300" Fill="Red"
Height="{Binding ActualHeight, ElementName=greenRect}"
Width="{Binding ActualWidth, ElementName=greenRect}"></Rectangle>
</Canvas>
<Slider Maximum="500" Minimum="0"
Value="{Binding Height, ElementName=grid, Mode=TwoWay}"></Slider>
<Slider Maximum="500" Minimum="0"
Value="{Binding Width, ElementName=grid, Mode=TwoWay}"></Slider>
</StackPanel>
我希望 green
和 red
矩形的大小相同,而在 WPF 的世界里一切都很好……但 Silverlight 无法绑定到 ActualHeight
和 ActualWidth
。那么,解决方案是什么?很简单,我们将创建一个包装器 SizeWrapper
,它有三个属性:RealWidth
、RealHeight
和 Element
(目标元素)。
它将监听目标元素的 SizeChanged
事件并更新其两个属性。这是代码
public class SizeWrapper : FrameworkElement
{
public FrameworkElement Element
{
get
{
return (FrameworkElement)GetValue(ElementProperty);
}
set
{
SetValue(ElementProperty, value);
}
}
public double RealHeight
{
get
{
return (double)GetValue(RealHeightProperty);
}
set
{
SetValue(RealHeightProperty, value);
}
}
// Using a DependencyProperty as the backing store for RealHeight.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty RealHeightProperty =
DependencyProperty.Register("RealHeight",
typeof(double), typeof(SizeWrapper), Helper.CreateMetadata(0.0));
public double RealWidth
{
get
{
return (double)GetValue(RealWidthProperty);
}
set
{
SetValue(RealWidthProperty, value);
}
}
// Using a DependencyProperty as the backing store for RealWidth.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty RealWidthProperty =
DependencyProperty.Register("RealWidth",
typeof(double), typeof(SizeWrapper), Helper.CreateMetadata(0.0));
// Using a DependencyProperty as the backing store
// for Element. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ElementProperty =
DependencyProperty.Register("Element", typeof(FrameworkElement),
typeof(SizeWrapper), Helper.CreateMetadata(null, OnElementChanged));
private static void OnElementChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs args)
{
var wrapper = (SizeWrapper)sender;
var oldElement = args.OldValue as FrameworkElement;
var newElement = args.NewValue as FrameworkElement;
if(oldElement != null)
oldElement.SizeChanged -= wrapper.SizeChanged;
if(newElement != null)
{
newElement.SizeChanged += wrapper.SizeChanged;
wrapper.UpdateSize(new Size(newElement.ActualWidth,
newElement.ActualHeight));
}
}
void SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateSize(e.NewSize);
}
private void UpdateSize(Size size)
{
RealHeight = size.Height;
RealWidth = size.Width;
}
}
现在,这是稍作修改的版本。唯一的修改是 red
矩形绑定到包装器的属性,而不是直接绑定到 green
矩形。正如你所料,这很有效。包装器位于 canvas
中。
<StackPanel>
<Canvas Height="500" Width="500" >
<wrappers:SizeWrapper x:Name="sizeWrapper"
Element="{Binding ElementName=greenRect}"></wrappers:SizeWrapper>
<Grid x:Name="grid" Canvas.Top="0"
Height="100" Width="100">
<Rectangle x:Name="greenRect" Fill="Green"></Rectangle>
</Grid>
<Rectangle Canvas.Top="300" Fill="Red"
Height="{Binding RealHeight, ElementName=sizeWrapper}"
Width="{Binding RealWidth, ElementName=sizeWrapper}"></Rectangle>
</Canvas>
<Slider Maximum="500" Minimum="0"
Value="{Binding Height, ElementName=grid, Mode=TwoWay}"></Slider>
<Slider Maximum="500" Minimum="0"
Value="{Binding Width, ElementName=grid, Mode=TwoWay}"></Slider>
</StackPanel>
示例 2 - 元素隐藏器
有些人可能会说:好吧,这只是用于特定问题的解决方法;稍等片刻,看看第二个示例,你就会欣赏它的简洁之处了!
这个例子应该值得单独写一篇文章,因为我怀疑很多人会想要它。
这次,我们将使用一个名为“ElementHidderWrapper
”的包装器来将元素“推”到屏幕边缘(就像你在 Visual Studio 的可停靠视图中所做的那样)。
如果 Show
是 0
,则目标完全折叠;如果它是 1.0
,则完全可见。MinMargin
是当 Show
等于 0
时要显示的最小边距。HideSide
是元素将隐藏的一侧。使用方法如下
<Grid>
<wrappers:ElementHidderWrapper
x:Name="hidder"
Element="{Binding ElementName=border}"
MinMargin="20"
HideSide="Left"
Show="1.0"
></wrappers:ElementHidderWrapper>
<Border x:Name="border" HorizontalAlignment="Left"
VerticalAlignment="Top" BorderThickness="1.0"
Width="100" Height="300"
BorderBrush="Black"
CornerRadius="0,10,10,0" Background="Green">
<Grid>
<TextBlock Text="blabla"
HorizontalAlignment="Center"
VerticalAlignment="Top"></TextBlock>
<CheckBox IsChecked="True"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Click="CheckBox_Click">
</CheckBox>
</Grid>
</Border>
</Grid>
目标元素是边框。最初,一切都显示出来。MinMargin
设置为 20 像素;这样,我们始终可以单击 checkbox
。当你单击 checkbox
时,它将隐藏/显示左侧的边框。这很容易做到,我只需要在我的包装器的 Show
属性上触发动画。
private void CheckBox_Click(object sender, RoutedEventArgs e)
{
CheckBox checkBox = (CheckBox)sender;
Storyboard storyBoard = new Storyboard();
DoubleAnimation showAnimation = new DoubleAnimation();
Storyboard.SetTarget(showAnimation, hidder);
Storyboard.SetTargetProperty(showAnimation, new PropertyPath("Show"));
showAnimation.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 400));
showAnimation.To = checkBox.IsChecked.Value ? 1.0 : 0.0;
storyBoard.Children.Add(showAnimation);
storyBoard.Begin();
}
包装器的代码不是重点,但我会快速解释一下:每当包装器的属性发生变化时,我都会重新计算目标的边距,并且完成了。
public enum HideSide
{
Top,
Bottom,
Left,
Right
}
public class ElementHidderWrapper : FrameworkElement
{
public double MinMargin
{
get
{
return (double)GetValue(MinMarginProperty);
}
set
{
SetValue(MinMarginProperty, value);
}
}
// Using a DependencyProperty as the backing store for MaxShow.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty MinMarginProperty =
DependencyProperty.Register("MinMargin", typeof(double),
typeof(ElementHidderWrapper),
Helper.CreateMetadata(0.0, MinMarginChanged));
private static void MinMarginChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs args)
{
ElementHidderWrapper hidder = (ElementHidderWrapper)sender;
hidder.UpdateElement();
}
public HideSide HideSide
{
get
{
return (HideSide)GetValue(HideSideProperty);
}
set
{
SetValue(HideSideProperty, value);
}
}
// Using a DependencyProperty as the backing store
// for HideSide. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HideSideProperty =
DependencyProperty.Register("HideSide", typeof(HideSide),
typeof(ElementHidderWrapper), Helper.CreateMetadata(HideSide.Bottom));
public double Show
{
get
{
return (double)GetValue(ShowProperty);
}
set
{
SetValue(ShowProperty, value);
}
}
// Using a DependencyProperty as the backing store for Show.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ShowProperty =
DependencyProperty.Register("Show", typeof(double),
typeof(ElementHidderWrapper), Helper.CreateMetadata(1.0, ShowChanged));
public FrameworkElement Element
{
get
{
return (FrameworkElement)GetValue(ElementProperty);
}
set
{
SetValue(ElementProperty, value);
}
}
// Using a DependencyProperty as the backing store for Element.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ElementProperty =
DependencyProperty.Register("Element",
typeof(FrameworkElement), typeof(ElementHidderWrapper),
Helper.CreateMetadata(null, ElementChanged));
private static void ElementChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs args)
{
ElementHidderWrapper hidder = (ElementHidderWrapper)sender;
FrameworkElement oldValue = args.OldValue as FrameworkElement;
FrameworkElement newValue = args.NewValue as FrameworkElement;
if(oldValue != null)
oldValue.SizeChanged -= hidder.SizeChanged;
if(newValue != null)
newValue.SizeChanged += hidder.SizeChanged;
hidder.UpdateElement();
}
private void SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateElement();
}
private static void ShowChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs args)
{
ElementHidderWrapper hidder = (ElementHidderWrapper)sender;
hidder.UpdateElement();
}
private void UpdateElement()
{
if(Element == null)
return;
var maxValue = GetMaxShowValue(Element);
var minValue = MinMargin;
var calculatedShowValue = minValue + (maxValue - minValue) * Show;
var marginValue = maxValue - calculatedShowValue;
SetMarginValue(Element, marginValue);
}
private void SetMarginValue(FrameworkElement element, double marginValue)
{
if(HideSide == HideSide.Left)
{
element.Margin = new Thickness(-marginValue,
element.Margin.Top, element.Margin.Right, element.Margin.Bottom);
}
else if(HideSide == HideSide.Top)
{
element.Margin = new Thickness(element.Margin.Left,
-marginValue, element.Margin.Right, element.Margin.Bottom);
}
else if(HideSide == HideSide.Right)
{
element.Margin = new Thickness(element.Margin.Left,
element.Margin.Top, -marginValue, element.Margin.Bottom);
}
else if(HideSide == HideSide.Bottom)
{
element.Margin = new Thickness(element.Margin.Left,
element.Margin.Top, element.Margin.Right, -marginValue);
}
}
private double GetMaxShowValue(FrameworkElement element)
{
if(HideSide == HideSide.Bottom || HideSide == HideSide.Top)
return element.ActualHeight;
else
return element.ActualWidth;
}
}
结论
这个模式不是什么大新闻,但是,给事物命名有助于人们使用并记住它适合什么地方。这个模式绝对应该放在 WPF/SL 开发者的工具箱中。