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

Silverlight 中健壮的用户控件导航

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (6投票s)

2011 年 2 月 18 日

CPOL

6分钟阅读

viewsIcon

26898

downloadIcon

777

一种在 Silverlight 项目中管理 UserControl 之间导航的简单而非常有效的方法。

引言

最近,我一直在进行很多 Silverlight 项目。这些项目都有一个共同点,那就是它们都由许多 UserControl 组成,这些 UserControl 之间会互相导航。例如,第一个控件将是 Login.xaml,一旦通过身份验证,它就需要导航到主应用程序。然后主应用程序有一个导航部分(例如菜单等),在其侧面将是当前控件,它可能需要导航到另一个控件。依此类推。

所以,对我来说,效果很好的一招是创建一个充当页面控制器并执行所有必需的过渡和加载的用户控件。你只需要将一个 UIElement(在几乎所有情况下都是 UserControl)传递给它,它就会加载它,然后替换屏幕上当前显示的内容。

使用代码

  1. 创建一个新的 **Silverlight 应用程序**项目或转到现有项目。所有导航将由一个 UserControl 控制。因此,向项目添加一个名为 NavController.xaml 的新 UserControl
  2. NavController.xaml 将有一个主网格(LayoutRoot),然后里面会有一个边框(ControlContainer),上面会有一个原始控件的图像(ImageOverlay)(稍后详述),然后是上面的灰色叠加层(GrayOverlay),最后是所有这些之上的一个旋转加载器(Loader)。
  3. structure.png

    应该放在 NavController.xaml 中的 XAML 代码如下:

    <Grid x:Name="LayoutRoot">
        <Border x:Name="ContentContainer" 
           VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
        </Border>
        <Image Opacity="0" x:Name="ImageOverlay" 
              VerticalAlignment="Stretch" 
              HorizontalAlignment="Stretch" />
        <Rectangle Opacity="0" x:Name="GrayOverlay" 
                VerticalAlignment="Stretch" 
                HorizontalAlignment="Stretch">
            <Rectangle.Fill>
                <RadialGradientBrush>
                    <GradientStop Color="#55000000" Offset="0" />
                    <GradientStop Color="#C5000000" Offset="1" />
                </RadialGradientBrush>
            </Rectangle.Fill>
        </Rectangle>
        <Border Opacity="0" VerticalAlignment="Center" 
            HorizontalAlignment="Center" x:Name="Loader"></Border>
    </Grid>
  4. 现在进入 NavController.cs。我们需要创建一个方法,该方法接受一个 UIElement 并执行所有将其显示在屏幕上的工作。以下是即将发生的事情的分解:
    1. 该方法接收一个 UIElement
    2. 我们截取屏幕上当前显示的内容的屏幕截图,并在 ImageOverlay 控件中显示它。
    3. 显示 ImageOverlayGrayOverlayLoader 控件。
    4. ContentContainer.Child 替换为新的 UIElement
    5. 在新控件的 Loaded 事件中,调用 NavController 中的另一个方法来隐藏步骤 3 中的控件。

    现在快速解释一下步骤 2 和 5:我们可以直接用新控件替换 ContentContainer 的子控件,而不进行图像处理,但这样用户会看到控件发生变化(如闪烁),这看起来一点也不流畅。因此,我们截取其屏幕截图并显示它,然后新控件可以在后台执行所有加载工作。例如,新控件可能会调用 Web 服务并花费几秒钟加载。这没关系,因为一旦该控件的初始加载完成,它就会调用停止加载。或者,如果没有服务调用,它可以在 Loaded 事件中设置停止加载。

    所以,首先,我们需要创建故事板来显示和隐藏步骤 3 中的那些控件。我正在做一个简单的淡入淡出效果,但你真的可以做任何事情。

    这将放在上面网格的下方

    <UserControl.Resources>
        <Storyboard x:Name="DisplayOverlays" 
              Completed="DisplayOverlays_Completed">
            <DoubleAnimation To="1" 
                    Storyboard.TargetName="ImageOverlay" 
                    Storyboard.TargetProperty="Opacity" 
                    Duration="00:00:01">
                <DoubleAnimation.EasingFunction>
                    <SineEase />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
            <DoubleAnimation To="1" 
                     Storyboard.TargetName="GrayOverlay" 
                     Storyboard.TargetProperty="Opacity" 
                     Duration="00:00:01">
                <DoubleAnimation.EasingFunction>
                    <SineEase />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
            <DoubleAnimation To="1" 
                     Storyboard.TargetName="Loader" 
                     Storyboard.TargetProperty="Opacity" 
                     Duration="00:00:01">
                <DoubleAnimation.EasingFunction>
                    <SineEase />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>
        <Storyboard x:Name="HideOverlays" 
                 Completed="HideOverlays_Completed">
            <DoubleAnimation To="0" 
                    Storyboard.TargetName="ImageOverlay" 
                    Storyboard.TargetProperty="Opacity" 
                    Duration="00:00:01">
                <DoubleAnimation.EasingFunction>
                    <SineEase />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
            <DoubleAnimation To="0" 
                    Storyboard.TargetName="GrayOverlay" 
                    Storyboard.TargetProperty="Opacity" 
                    Duration="00:00:01">
                <DoubleAnimation.EasingFunction>
                    <SineEase />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
            <DoubleAnimation To="0" Storyboard.TargetName="Loader" 
                      Storyboard.TargetProperty="Opacity" 
                      Duration="00:00:01">
                <DoubleAnimation.EasingFunction>
                    <SineEase />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>
    </UserControl.Resources>

    方法中我们要做的第一件事是将传入的控件设置为一个类变量,以便在显示动画完成后可以使用它。接下来,我们使用 WritableBitmap 截取 ContentContainer 元素的屏幕截图(这并不完全准确,但你明白我的意思)。然后我们将 ImageOverlay 控件的源设置为该图像,然后显示所有叠加层(尽管它们的透明度仍为 0,因此它们不会被看到),最后启动动画以淡入叠加层。

    private UIElement _waitingControl = null;
    
    public void NavigateToControl(UIElement newControl)
    {
        _waitingControl = newControl;
        //take screenshot
        WriteableBitmap bitmap = new WriteableBitmap(ContentContainer, null);
        ImageOverlay.Source = bitmap;//set the source of the image control
        bitmap.Invalidate();//kill the bitmap
        ImageOverlay.Visibility = System.Windows.Visibility.Visible; 
        GrayOverlay.Visibility = System.Windows.Visibility.Visible;
        Loader.Visibility = System.Windows.Visibility.Visible;
        DisplayOverlays.Begin();
    }

    用于隐藏叠加层的(当新控件完成所需工作后将调用此方法)的方法几乎是不言自明的。

    public void Hide()
    {
        HideOverlays.Begin();
        //start hiding all the overlays to display the new control
    }

    当叠加层的淡入动画完成并且我们隐藏了底层控件后,我们就可以将其更改为新控件(由于叠加层,用户不会看到此更改)。

    private void DisplayOverlays_Completed(object sender, EventArgs e)
    {
        ContentContainer.Child = _waitingControl;
        // change the control to the new one
    }

    一旦叠加层被隐藏(就其透明度而言),我们就需要折叠它们。这是因为即使它们是透明的,你仍然无法单击穿过它们。

    private void HideOverlays_Completed(object sender, EventArgs e)
    {
        ImageOverlay.Visibility = System.Windows.Visibility.Collapsed; 
        GrayOverlay.Visibility = System.Windows.Visibility.Collapsed;
        Loader.Visibility = System.Windows.Visibility.Collapsed;
    }
  5. 好的,现在实际的控件基本上完成了,我们需要一种方法让任何控件都能调用其父 NavController。为此,我们将有一个全局静态类,其中包含一些巧妙的方法。添加一个名为 Global.cs 的新类。在其中,放入以下内容:
  6. public static class Global
    {
        public static T FindParent<T>(UIElement control) where T : UIElement
        {
            UIElement p = VisualTreeHelper.GetParent(control) as UIElement;
            if (p != null)
            {
                if (p is T)
                    return p as T;
                else
                    return FindParent<T>(p);
                }
                return null;
            }
     
            public static void ChangeControl(this UIElement uc, UIElement newControl)
            {
                NavController controller = FindParent<NavController>(uc);
     
                if (controller != null)
                    controller.NavigateToControl(newControl);
            }
     
            public static void HideLoading(this UIElement uc)
            {
                NavController controller = FindParent<NavController>(uc);
    
                if (controller != null)
                    controller.Hide();
            }
    }

    第一部分是一个非常有用的辅助方法,它会遍历视觉树并返回第一个匹配的实例。因此,我们将使用它,以便任何控件都可以找到它内部的第一个 NavController。接下来的两个方法是为了更方便地停止加载或从任何 UIElement 导航到新控件。注意参数列表中的 this?这意味着在任何 UIElement 上,我们现在都可以执行:myElement.HideLoading();。酷吧?请记住,这两个方法会调用 NavController 中的相应方法,但**不能**命名相同,否则会导致无限循环并可能摧毁宇宙。

  7. 我们离成功工作更近了一步!但首先,我们需要将我们的新 NavController 设置为第一个加载的控件(然后其他内容将在其中加载)。因此,打开 App.cs 并找到 Application_Startup 方法。将其中的当前行替换为以下内容:
  8. this.RootVisual = new NavController(); 
    (this.RootVisual as NavController).NavigateToControl(new Login()); 

    第一行只是使第一个加载的控件成为 NavController。然后下一行运行的方法告诉它更改为新控件。在这种情况下,我刚刚创建了一个名为 LoginUserControl,它将是登录界面。

    好的,正如前面提到的,每个以这种方式加载的控件都必须在完成后告诉 NavController 停止加载。在 Login 的情况下,这将在控件加载完成后发生,如下所示:

    public Login()
    {
        InitializeComponent();
        this.Loaded += (se, ev) =>
            {
                this.HideLoading();
            };
    }

    然后当用户单击按钮时,它可以切换到新控件(在本例中是 MainPage),如下所示:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        //check login etc
        this.ChangeControl(new MainPage());
    }

最后

下载内容包含上述内容,以及一些用于更好地解释它的控件。它还展示了如何将 NavController 嵌套在另一个 NavController 中。我很快会发布另一篇文章,介绍如何制作一个旋转加载器——但你可以在 Loader 控件中放入任何你想要的东西。你可以尝试下面的解决方案。

注意

我很感激评论——但请记住,这是我的第一篇文章,所以不要太刻薄 :P

你可以在我的博客上查看原始帖子并**观看演示**:RogueCode

历史

  • 2/18/2011 - 文章撰写。
© . All rights reserved.