65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight 和 WPF 的包装器模式

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2010年2月14日

Ms-PL

3分钟阅读

viewsIcon

37370

downloadIcon

235

一种设计模式,可轻松绑定或为元素中不存在的属性设置动画,并且该模式同时适用于 Silverlight 和 WPF。

目录

引言

距离我上一篇文章已经很久了。这篇文章很短,而且不像我以前的文章那样有创意,但我希望你会喜欢! :)

我希望你们没有人曾经特别谈论过这一点,但你们可能已经使用过我今天要讲的内容,只是没有命名它:WPF 和 Silverlight 中的包装器模式。

模式表

模式类型:表示模式。

问题:你想为一个不存在于对象(目标对象)上的属性创建动画或绑定,或者该属性不支持 Storyboard 或 Binding。

解决方案:创建一个对象(包装器),它将公开该属性,并相应地修改目标对象。

替代方案:

  • 使用附加属性;但是,这在 Silverlight 3.0 中会失败(它在 XAML 中不支持对附加属性进行动画处理)。
  • 使用装饰器,但 Silverlight 中不存在装饰器,而且有时会使你的 XAML 更难阅读。

Actor:

  • 我们想要设置动画的对象称为“target”。
  • 我们将创建的用于公开新属性以进行动画处理的对象称为“wrapper”。

具体示例

  • 第一个是 Silverlight 中的一个常见问题:绑定到 ActualWidth/Height 属性。
  • 第二个是一个元素隐藏器;它允许你显示或“推”一个元素到屏幕边缘(就像 Visual Studio 中的可停靠视图一样)。

对于这两个示例,包装器模式提供了一个非常好的解决方案。

示例 1 - 绑定到 ActualWidth 和 ActualHeight

这是示例

滑块绑定到包装绿色矩形的网格的 HeightWidth。红色矩形的 widthheight 绑定到绿色矩形的 ActualWidthActualHeightgreen 矩形的 HeightWidth 属性未设置。

这是代码

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

我希望 greenred 矩形的大小相同,而在 WPF 的世界里一切都很好……但 Silverlight 无法绑定到 ActualHeightActualWidth。那么,解决方案是什么?很简单,我们将创建一个包装器 SizeWrapper,它有三个属性:RealWidthRealHeightElement(目标元素)。

它将监听目标元素的 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 的可停靠视图中所做的那样)。

如果 Show0,则目标完全折叠;如果它是 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 开发者的工具箱中。

© . All rights reserved.