WPF 中的动态用户界面






4.64/5 (19投票s)
回顾如何在 WPF 中实现动态用户界面。
引言
本文探讨了如何使用 Windows Presentation Foundation (WPF) 设计和创建动态用户界面。在此过程中,我们将探讨一个演示应用程序,该应用程序使用 XAML 创建动态用户界面,以便向用户显示丰富而丰富的交互式警报消息。
背景
“动态用户界面”一词指的是一种用户界面,它可以托管在运行时发现的任意内容。传统上,桌面应用程序由预先设计和编译的用户界面组成,然后在应用程序部署后保持相对静态。传统桌面应用程序的静态性质限制了可以显示的内容类型。其中许多限制已通过基于 Web 的 UI 平台更容易解决,因为 Web UI 始终是下载的,并且基于标记(即 HTML)。
随着 WPF 的出现,在应用程序运行时下载和显示用户界面的部分内容变得更加容易。您不仅可以轻松显示任意 UI 内容,还可以为这些视觉元素关联运行时行为。例如,创建一个 WPF 应用程序,该应用程序下载其用户界面的一部分,然后响应动态加载的按钮被用户点击,这是相当容易的。这为桌面应用程序开发人员打开了广泛的新可能性。
没有完美
本文展示了一种在 WPF 中创建动态用户界面的方法。当然,您还可以使用其他方法,例如下载包含 UserControl
的程序集,然后使用反射将该控件的实例加载到您的 UI 中。本文概述的方法是基于 XAML 的,这使其非常灵活,并且允许您通过 XML Web 服务公开用户界面的部分内容。
自然,特定问题的每种解决方案都有其优点和缺点,因此没有绝对的“最佳”依赖方式。例如,本文所示的方法期望应用程序知道如何响应用户与动态 UI 内容中的元素的交互。如果用户单击动态加载的 Hyperlink
控件,应用程序必须相应地进行响应。涉及动态加载的 UserControl
的方法不会有此限制,因为 UserControl
可以包含自己的交互逻辑。但是,由于 UserControl
不能作为 XML 下载,因此如果您选择将动态 UI 的部分内容下载为 DLL,则可能会遇到防火墙/安全问题。
三大支柱
如果您选择使用本文描述的方法,则创建动态用户界面需要三个基本步骤。我们将在本节中回顾这三个支柱,然后在下一节中将它们投入使用。
XAML
动态加载的 UI 内容必须以 XAML 格式进行序列化。换句话说,您的应用程序必须能够访问一些 XAML 并将其转换为可以显示的实时视觉元素。此过程中的秘密配料是 XamlReader.Load
方法。XamlReader.Load
内容容器
托管动态 UI 内容的应用程序必须有一个放置该内容的位置。将一些 XAML 转换为视觉元素后,您需要将它们显示在某个地方。您可以使用 ContentControl
或 ContentPresenter
来包含视觉元素(阅读有关这两个元素之间差异的信息 此处)。
交互通知
您的动态 UI 不应该是花瓶。如果它除了好看之外什么都不做,您可能就不需要它了。您有两种选择可以将行为与动态加载的视觉元素关联起来:路由事件和路由命令。
由于路由事件/命令可以冒泡到元素树,因此您可以在编译时将基本交互逻辑添加到应用程序中。当动态内容加载并且用户与之交互时,您预先构建的交互逻辑可以执行必要的操作以响应用户输入。
例如,如果您的动态 UI 通常包含 Hyperlink
,那么您的应用程序应提供一种响应 Hyperlink
被点击的方法。如前所述,应用程序必须具有处理用户交互的内置逻辑是使用基于 XAML 的方法创建动态 UI 的一个缺点。但是,对于具有相对简单动态 UI 需求的应用程序来说,这个缺点可能不是问题。
演示应用程序做什么
本文附带了一个演示应用程序,该应用程序演示了如何实现动态 UI。演示应用程序是一个“虚构”的业务应用程序(即,它没有任何实际功能),它向用户显示动态警报消息。
这种动态用户界面的想法是,可以将警报消息放在服务器上,当应用程序运行时,它会检索并显示消息。警报存储为 XML,碰巧包含有效的 XAML(请记住,XAML 是一种基于 XML 的语言)。应用程序可以通过 XML Web 服务、数据库调用或网络驱动器等方式检索警报 XML 数据。
此动态警报消息功能允许应用程序在不知道警报消息可能包含什么或何时存在的情况下进行部署。当服务器上放置警报消息时,应用程序可以简单地下载它并显示它所包含的任何内容。由于警报是用 XAML 声明的,因此它可以利用整个 WPF 平台;例如,包括图像、视频、音频,甚至是可以响应用户输入的控件。
演示应用程序的外观
这是演示应用程序运行时动态加载的警报消息的屏幕截图
除了灰色的标题栏和底部的“[关闭]”链接之外,该警报界面中的所有内容都是动态加载的。如果您单击“此处”链接,它将打开一个 Web 浏览器到一个特定页面。警报消息使用 WPF 中的流文档技术,通过项目符号列表提供流畅的阅读体验。在 Windows Forms 应用程序中模拟此功能肯定不是一件简单的事情。
演示应用程序如何工作
现在我们已经牢固理解了什么是动态用户界面以及在 WPF 中实现它们所需的基本步骤,让我们看看演示应用程序是如何工作的。这幅拼图中包含五个部分。
警报 XML 数据
首先,我们将看看如何声明警报。警报保存为 XML,可以在运行时从服务器检索。这是演示应用程序中使用的警报 XML 数据
<?xml version="1.0" encoding="utf-8" ?>
<Alert Title="Company News">
<!-- The inner XML of the <Alert> element is valid XAML. -->
<FlowDocumentScrollViewer
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
VerticalScrollBarVisibility='Auto'
>
<FlowDocument>
<Paragraph>
The Foo Daddy Company is proud to announce
that it has a new Vice President, Ronald McDonald.
Mr. McDonald joins us after a long tenure at the
McDonald's Corporation, serving as Chief Happiness
Officer for over 20 years. Ronald McDonald can
speak 31 different languages including:
</Paragraph>
<List>
<ListItem>
<Paragraph>Mandarin</Paragraph>
</ListItem>
<ListItem>
<Paragraph>Dutch</Paragraph>
</ListItem>
<ListItem>
<Paragraph>Tagalog</Paragraph>
</ListItem>
<ListItem>
<Paragraph>Hindi</Paragraph>
</ListItem>
</List>
<Paragraph>
Read more about Mr. McDonald's background
<Hyperlink
NavigateUri='http://en.wikipedia.org/wiki/Ronald_Mcdonald'
ToolTip="View Mr. McDonald's profile">
here
</Hyperlink>.
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
</Alert>
上面 XML 数据中有两个重要方面。根 <Alert>
元素有一个 Title
属性。标题显示在用户界面的警报内容上方。<Alert>
元素的内部 XML 是构成警报消息的 XAML。该 XAML 由宿主应用程序动态加载和呈现。
Alert 类
警报的标题和内容需要存储在某个地方。我选择将这些信息存储在一个名为 Alert
的简单类中。这是整个 Alert
类
/// <summary>
/// Stores information about an alert.
/// </summary>
public class Alert
{
readonly object content;
readonly string title;
public Alert( string title, object content )
{
if( content == null )
throw new ArgumentNullException( "content" );
// If the alert's title was not specified use a default value.
this.title = String.IsNullOrEmpty(title) ? "Alert" : title;
this.content = content;
}
public object Content
{
get { return this.content; }
}
public string Title
{
get { return this.title; }
}
}
主应用程序窗口
在此演示应用程序中,主 Window
实际上并没有做什么。它没有真正的功能。它所做的只是检查是否存在警报,然后显示警报(如果存在)。在实际应用程序中,主 Window
当然会有更多内容,但为了演示起见,我们保持简单。这是主 Window
的代码隐藏
/// <summary>
/// This is the main Window of an imaginary business app.
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// When the Window loads, check to see if
// there is an alert to display.
this.Loaded += delegate
{
this.CheckForAvailableAlert();
};
}
/// <summary>
/// Checks to see if an alert needs to be shown.
/// If so, it shows the alert message in a Window.
/// </summary>
void CheckForAvailableAlert()
{
Alert alert = AlertProvider.GetLatestAlert();
if( alert != null )
new AlertWindow( this, alert ).ShowDialog();
}
}
AlertProvider
主 Window
依赖于一个名为 AlertProvider
的类来确定是否存在要显示给用户的警报。AlertProvider
负责“检查”是否存在要显示的警报,如果存在,则返回有关该警报的信息。请记住,在此演示中,实际警报数据并未从外部源检索。警报 XML 是应用程序程序集中的一个资源,但在实际应用程序中,警报将从外部源检索。这是 AlertProvider
的唯一公共成员
/// <summary>
/// This method retrieves an alert to display.
/// If no alert is available it returns null.
/// </summary>
public static Alert GetLatestAlert()
{
Alert alert = null;
XmlTextReader xmlRdr = null;
try
{
xmlRdr = RetrieveAlertXml();
bool alertExists = xmlRdr != null;
if( alertExists )
alert = CreateAlertFromXml( xmlRdr );
}
finally
{
if( xmlRdr != null )
xmlRdr.Close();
}
return alert;
}
该方法依赖于两个私有帮助方法;RetrieveAlertXml
和 CreateAlertFromXml
。前者实现如下
/// <summary>
/// In this demo we keep it simple and just load some XML
/// out of a resource in this assembly. In a real app you
/// might want to access a Web service in this method, to
/// get the latest XML alert message, if one exists.
/// </summary>
static XmlTextReader RetrieveAlertXml()
{
try
{
Uri uri = new Uri( "AlertData.xml", UriKind.Relative );
StreamResourceInfo info = Application.GetResourceStream( uri );
return new XmlTextReader( info.Stream );
}
catch( Exception ex )
{
Debug.WriteLine( "Did not get Alert XML: " + ex );
return null;
}
}
下面可以看到将检索到的 XML 转换为 Alert
对象的代码
/// <summary>
/// Creates an Alert object based on the specified XML.
/// </summary>
/// <param name="xmlRdr">Contains data about an alert.</param>
static Alert CreateAlertFromXml( XmlTextReader xmlRdr )
{
// Load the XML data into an XmlDocument.
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load( xmlRdr );
// Get the root <Alert> element.
XmlElement alertElem = xmlDoc.DocumentElement;
// Grab the alert's title off the <Alert> element.
string title = alertElem.GetAttribute( "Title" );
// Create visual objects out of the <Alert>
// element's inner XML (which must be valid XAML).
object content = DeserializeXaml( alertElem.InnerXml );
// Bundle up the information we retrieved and return it.
return new Alert( title, content );
}
将充满 XAML 的字符串转换为视觉元素的真正魔力在这里实现
/// <summary>
/// Instantiates and returns the object(s) declared
/// in the specified XAML.
/// </summary>
/// <param name="xaml">Valid XAML markup text.</param>
static object DeserializeXaml( string xaml )
{
using( MemoryStream stream = new MemoryStream() )
{
// Convert the text into a byte array so that
// it can be loaded into the memory stream.
byte[] bytes = Encoding.UTF8.GetBytes( xaml );
// Write the XAML bytes into a memory stream.
stream.Write( bytes, 0, bytes.Length );
// Reset the stream's current position back
// to the beginning so that when it is read
// from, the read begins at the correct place.
stream.Position = 0;
// Convert the XAML into a .NET object.
return XamlReader.Load( stream );
}
}
AlertWindow
最后但同样重要的是,我们有 Window
,它充当动态内容容器和交互逻辑的来源。AlertWindow
类用作模式对话框来显示动态加载的 UI 内容。这是 AlertWindow
的 XAML 声明的一部分
<!-- Add a handler to the RequestNavigate event of Hyperlink
so that if a dynamically loaded Hyperlink is clicked,
we can open the Web page to which it points. -->
<Grid Hyperlink.RequestNavigate="OnHyperlinkRequestNavigate">
<!-- Associate an event handling method with the Executed
event of the Close command. -->
<Grid.CommandBindings>
<CommandBinding
Command="{x:Static ApplicationCommands.Close}"
Executed="OnCloseCommandExecuted"
/>
</Grid.CommandBindings>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- ALERT TITLE HEADER -->
<TextBlock Grid.Row="0"
Background="Gray"
FontSize="23"
Foreground="White"
Text="{Binding Path=Title}"
TextAlignment="Center"
/>
<!-- ALERT CONTENT CONTAINER -->
<ContentControl Grid.Row="1"
Content="{Binding Path=Content}"
/>
<!-- [CLOSE] HYPERLINK -->
<TextBlock Grid.Row="2"
FontSize="15"
HorizontalAlignment="Center"
Margin="6"
>
[<Hyperlink Command="Close">Close</Hyperlink>]
</TextBlock>
</Grid>
如上所述,包含所有其他元素的 Grid
面板具有两个有趣的设置。附加事件语法用于为 Hyperlink
类的 RequestNaviate
冒泡路由事件添加处理程序。这确保当用户单击动态加载的 Hyperlink
时,代码隐藏中的 OnHyperlinkRequestNavigate
方法将被调用。Grid
还为 Close
命令建立了 CommandBinding
。该绑定确保在用户想要关闭 AlertWindow
时通知它。
这是 AlertWindow
的代码隐藏
/// <summary>
/// Displays an alert message.
/// </summary>
public partial class AlertWindow : System.Windows.Window
{
public AlertWindow( Window owner, Alert alert )
{
InitializeComponent();
this.Owner = owner;
this.DataContext = alert;
}
void OnCloseCommandExecuted( object sender, ExecutedRoutedEventArgs e )
{
this.Close();
}
void OnHyperlinkRequestNavigate( object sender, RequestNavigateEventArgs e )
{
Process.Start( e.Uri.AbsoluteUri );
}
}
结论
本文展示了 WPF 应用程序中动态用户界面的激动人心的潜力。通过使用 XAML、内容容器和路由交互通知,我们能够利用运行时任意的 UI 内容。还值得注意的是,本文描述的技术并不是创建动态 UI 的唯一方法。在您的应用程序中实现动态 UI 之前,请务必考虑其他选项,以确保使用最合适的技术。