Skip to content

Latest commit

 

History

History
540 lines (399 loc) · 30.9 KB

07.md

File metadata and controls

540 lines (399 loc) · 30.9 KB

七、表单用户界面

前一章讨论了使用 F# 创建自定义图形。在本章中,我们将了解如何在 F# 中创建基于表单的应用。有许多不同的技术可以在中创建基于表单的应用。NET,包括 Windows 窗体、WPF、Silverlight 和 GTK#。由于这些技术都是基于相似的想法,我们将继续关注一个:WPF。

简单的形式

WPF 允许您以两种方式定义表单:第一种是直接操作 WPF 对象,第二种是用一种叫做 XAML 的 XML 方言定义表单。XAML 是有用的,因为有几个不同的用户界面设计包可用,允许设计师在 XAML 创造丰富的用户体验。然后,设计人员可以将它们传递给开发人员,将它们连接到应用中。我们将在本章的后面讨论 XAML;现在,我们将了解如何直接使用 WPF 对象创建表单。

当您想要创建仅由几个控件组成的简单表单时,直接使用对象是一种很好的方法。在这种情况下,表单的创建可能足够简单,不值得使用设计师的努力。此外,以这种方式创建表单是一种很好的学习体验,因为它可以帮助您更好地理解每种控件类型是如何组合在一起的。

我们希望创建一个具有三个控件的窗体:一个文本框、文本框前面的描述性标签和紧接其下的按钮。为了创建我们想要的布局,我们将使用两个堆栈面板:一个水平面板用于保存标签和文本框,一个垂直面板用于保存水平堆栈面板及其正下方的按钮。

为了创建应用,我们将启动一个新的 F# 项目,并添加对PresentationCore.dllPresentationFramework.dllSystem.XAML.dll(尽管我们在此阶段没有使用任何 XAML,但这是必需的)以及最后的WindowsBase.dll的引用。然后,我们需要一些开放语句来提供对名称空间的简单访问:

    open System
    open System.Windows
    open System.Windows.Controls

我们几乎已经准备好创建我们的控件了,但是首先我们需要整理一下 WPF 的一个有点烦人的方面。在 WPF,有一个名为UIElementCollection的集合,用于存储子控件。这个集合有一个Add方法,它既有副作用——将控件添加到集合中——又有返回值——新添加的控件的索引。我们对控件的索引几乎没有任何兴趣,所以可以忽略返回值;然而,F# 会给我们一个警告,我们忽略了一个返回值,因为这通常是函数式编程中错误的指示。为了解决这个问题,我们需要添加一个简短的助手函数,在不返回索引的情况下将该项添加到控件集合中。在 WPF,并非所有控件都可以有子集合;只有继承自Panel的控件拥有Children集合。这就是为什么我们在下面的示例中向 helper 函数的第二个参数添加类型约束。该函数将只接受来自Panel的控制作为其第二个参数。addChild助手功能的实现也显示在下面的代码中。它只是将给定的控件添加到Children集合中,并忽略结果。

    // Adds a child to a panel control.
    let addChild child (control: Panel) =
        control.Children.Add child |> ignore

现在我们可以开始创建我们的控件了。我们将定义一个createForm函数来处理控件的创建。它真的没有魔力。我们只需创建水平堆栈面板,然后向其中添加标签和文本框。接下来,我们创建垂直堆栈面板,并添加水平堆栈面板,然后是垂直面板,然后是按钮控件,同时不要忘记连接按钮的事件处理程序。

    // Function to create the form interface.
    let createForm() =
        // Horizontal stack panel to hold label and text box.
        let spHorozontal =
            new StackPanel(Orientation = Orientation.Horizontal)

        // Add the label to the stack panel.
        spHorozontal |> addChild (new Label(Content = "A field",
                                            Width = 100.))

        // Add a text box to the stack panel.
        let text = new TextBox(Text = "<enter something>")
        spHorozontal |> addChild text

        // Create a second stack panel to hold our label
        // and a text box with a button below it.
        let spVert = new StackPanel()
        spVert |> addChild spHorozontal

        // Create the button and make it show the content
        // of the text box when clicked.
        let button = new Button(Content= "Press me!")
        button.Click.Add(fun _ -> MessageBox.Show text.Text |> ignore)
        spVert |> addChild button

        // Return the outermost stack panel.
        spVert

这就是创建表单的全部内容。为了显示表单,我们需要在一个窗口中托管它,然后创建一个 WPF 应用来创建一个事件循环并显示控件。这样做的代码如下:

    // Create the window that will hold the controls.
    let win = new Window(Content = createForm(),
                         Title = "A simple form",
                         Width = 300., Height = 150.)

    // Create the application object and show the window.
    let app = new Application()
    [<STAThread>]
    do app.Run win |> ignore

正如我们在上一章中看到的,当在 WPF 事件循环中启动时,我们需要确保STAThread属性被附加到启动方法调用中。在执行这个应用时,我们应该会看到以下表单:

图 6:在 F# 中创建的 WPF 表单

使用 XAML 的表单

虽然自己创建用户界面的方法可以很好地适用于简单的应用,但是如果我们想要更复杂的风格和效果,最好使用 XAML。在本例中,我们将看到创建一个与前一个表单具有完全相同的布局和功能的表单。唯一改变的是布局现在将在 XAML 定义,表单的行为将使用 F# 定义。我们的形式的 XAML 定义如下:

    <Window 
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="A XAML Form" Height="350" Width="525">
      <StackPanel>
        <StackPanel Orientation="Horizontal">
          <Label Width="100">A field</Label>
          <TextBox x:Name="MessageTextBox">&lt;enter something&gt;</TextBox>
        </StackPanel>
        <Button x:Name="PressMeButton">Press me!</Button>
      </StackPanel>
    </Window>

从 XAML 的定义中很容易看出,布局像以前一样由两个堆栈面板、一个标签、一个文本框和一个按钮组成。我们给文本框和按钮起了名字,因为这些是我们需要从代码中访问的对象。文本框称为MessageTextBox,按钮称为PressMeButton。与 C#相比,F# 与 XAML 的集成稍少,因此我们需要定义几个助手函数来帮助我们加载 XAML 并访问其中定义的控件。在这样的一个小例子中,这些助手所需的额外代码看起来是很大的开销,但是在一个更实际的应用中,随着助手函数在整个应用中被重用,这些额外函数的成本将很快分摊。

我们刚刚看到的 XAML 定义需要作为MainWindow.xaml添加到 F# 项目中。在该文件的属性窗口中,您需要将构建操作设置为EmbeddedResource,这样它将作为资源流嵌入到程序集清单中。这几乎是我们在 XAML 部分所需要做的。

在我们开始 F# 部分之前,我们需要引用与之前应用相同的组件(PresentationCore.dllPresentationFramework.dllSystem.XAML.dllSystemXML.dll),并且我们需要以下open语句。

    open System
    open System.Reflection
    open System.Windows
    open System.Windows.Markup
    open System.Windows.Controls

我们需要两个助手函数:一个加载 XAML 窗口的函数,一个帮助我们访问窗口中定义的控件的操作符。第一个功能很简单。要访问清单资源流,我们需要访问包含该资源的程序集对象。在这种情况下,由于我们的助手函数是在资源将要嵌入的同一个程序集中定义的,所以我们可以使用Assembly.GetExecutingAssembly()来获取对程序集对象的引用。例如,如果帮助器函数与资源文件不在同一个程序集中,它可能已经被移动到另一个程序集中,这样它就可以更容易地在项目之间共享。我们需要将程序集对象作为参数传递给函数。一旦我们有了资源流,我们就可以使用属于 WPF 框架的XAML Reader类来加载它。这就是我们的功能:

    // Load a XAML file from the current assembly.
    let loadWindowFromResources name =
        let currentAssem = Assembly.GetExecutingAssembly()
        use xamlStream = currentAssem.GetManifestResourceStream(name)
        // Resource stream will be null if not found.
        if xamlStream = null then
            failwithf "The resouce stream '%s' was not found" name
        XamlReader.Load(xamlStream) :?> Window

关于函数有几点值得注意。首先,我们使用use关键字而不是let来创建到我们的xamlStream标识符的绑定,该标识符代表资源流。use关键字相当于 C#中的using,意味着流一旦脱离范围就会被调用其Dispose方法。其次,我们对xam lStream标识符进行空检查,如果值为空,则引发异常。这是因为如果找不到与该名称匹配的资源流,方法GetManifestResourceStream将返回 null,在这种情况下,通过引发异常,我们将获得一个更有意义的异常。最后,我们使用XAML Reader.Load方法加载 XAML 文件。这个方法返回一个对象,所以我们需要转换成我们期望的对象的实际类型,在这个例子中是Window类。我们可以很容易地选择从磁盘上的文件中加载 XAML,而不是从清单中嵌入的文件中加载;在清单中使用文件的好处是,我们可以确保它与程序集一起分发。

我们需要定义的第二个功能是访问位于窗口内的控件的方法。继承自Window的基类FrameworkElement提供了一个FindName方法来查找我们的命名控件。一种可能是直接使用这种方法来找到我们感兴趣的控件:

    let pressMeButton = win.FindName("PressMeButton") :?> Button

虽然这是一种完全可以接受的方法,但是 F# 提供了一种有趣的替代方法,可以为您节省一点打字时间。F# 允许您定义一个自定义的动态运算符,其行为类似于 C#中的动态关键字。动态运算符是一个问号(?),它允许您进行看起来像方法或属性调用的操作,但将方法或属性名称恢复为字符串,以便您可以进行动态查找。下面是我们如何定义动态运算符:

    // Dynamic lookup operator for controls.
    let (?) (c:obj) (s:string) =
        match c with
        | :? FrameworkElement as c -> c.FindName(s) :?> 'T
        | _ -> failwith "dynamic lookup failed"

这里我们看到一个自定义操作符,后跟两个参数:第一个是 type object,第二个是 type string。第一个对象是我们动态调用的对象,第二个是被调用的属性或方法的名称。运算符模式的实现与对象相匹配,以检查它是否属于FrameworkElement类型。如果不是,我们抛出一个异常。如果是,我们称之为框架元素的FindName方法。如果目前还没有 100%清楚,不要担心;动态自定义操作符是 F# 更高级的功能之一。需要保留的是,现在定义这个运算符可以让您在窗口上进行动态查找,这可以用来找到我们感兴趣使用的按钮:

    let pressMeButton: Button = win?PressMeButton

现在我们已经有了这两个助手函数,我们准备创建我们的基于 XAML 的窗口。为此,我们将实现一个函数createMainWindow,它将负责创建窗口并将事件连接到相关控件。

    // Creates our main window and hooks up the events.
    let createMainWindow() =
        // Load the window from the resource file.
        let win = loadWindowFromResources "MainWindow.xaml"
        // Find the relevant controls in the window.
        let pressMeButton: Button = win?PressMeButton
        let messageTextBox: TextBox = win?MessageTextBox
        // Wire up an event handler.
        let onClick() =
            MessageBox.Show(messageTextBox.Text) |> ignore
        pressMeButton.Click.Add(fun _ -> onClick())
        // Return the newly created window.
        win

    // Create the window.
    let win = createMainWindow()

    // Create the application object and show the window.
    let app = new Application()
    [<STAThread>]
    do app.Run win |> ignore

createMainWindow的实现相当简单。我们使用助手功能loadWindowFromResource加载窗口本身。一旦我们有了窗口,我们就可以获取它包含的两个控件的引用:按钮pressMeButton和文本框messageTextBox。一旦我们有了这些引用,就很容易在按钮的点击事件中添加一个事件处理程序,并从这个事件处理程序中访问文本框的Text属性。为了完成我们的功能,只需要返回我们新创建的窗口。

要显示窗口,我们只需调用createMainWindow函数,然后启动 WPF 事件循环来显示窗口,就像我们在前面的示例中所做的那样。该窗口看起来将与上一个示例中的完全一样,但是现在窗口的布局在 XAML 定义,很容易添加更多的样式,因此控件看起来不会那么简单。

使用 MVVM 的表单

在 WPF 实现表单的一种常见方式是使用 MVVM 设计模式。对于那些可能不熟悉 MVVM 的人,让我们快速回顾一下这个设计模式是什么。MVVM 代表模型-视图-视图模型。在这种设计模式中,模型是表示数据或域的对象。视图是用户界面——在这种情况下,我们将使用 XAML,但是如果我们愿意,我们可以直接使用 WPF 对象。视图模型是位于视图和模型之间的对象层,用于提供它们之间的映射,并对图形用户界面中的事件和变化做出反应。视图通过 WPF 强大的数据绑定机制与视图模型通信。

我们将看看如何以 MVVM 风格实现一个简单的表单。MVVM 是一个很大的话题,这个例子并不是为了展示实现 MVVM 设计模式所涉及的所有技术。相反,它旨在让你尝试在 F# 中做 MVVM,并允许你应用你现有的 MVVM 知识,或来自其他 MVVM 文章的信息,到 F# MVVM 实现。我们要看的例子是如何用 MVVM 风格实现一个简单的主细节页面。我们正在构建的应用将用于查看我们的机器人库存。我们将看到一个机器人列表,并被允许点击一个机器人来查看更多关于它的细节。

正如我们已经说过的,MVVM 将代码分成三个不同的组件:模型、视图和视图模型。除此之外,我们将添加一个“存储库”,它将抽象数据访问逻辑。我们将按照以下顺序浏览应用的各个部分:我们将查看模型和存储库,然后是视图模型,最后是视图。

因为我们的应用是一些数据的简单只读视图,所以没有域逻辑。这意味着应用的模型部分非常简单——只需要一个数据容器。对于数据容器,我们将使用 F# 记录类型:

    type Robot =
        { Name: string
          Movement: string
          Weapon: string }

如你所见,我们将只存储关于我们机器人的三条信息:它的名字、它如何移动以及它的武器。我们的应用非常简单,这就是我们需要的模型。为了简单起见,我们将对存储库中的数据进行硬编码,但是在一个更实际的例子中,这将来自数据库。

    type RobotsRepository() =
        member x.GetAll() =
            seq{ yield {Name = "Kryten"
                        Movement = "2 Legs"
                        Weapon = "None" }
                 yield {Name = "R2-D2"
                        Movement = "3 Legs with wheels"
                        Weapon = "Electric sparks" }   
                 yield {Name = "Johnny 5"
                        Movement = "Caterpillars"
                        Weapon = "Laser beam" }
                 yield {Name = "Turminder Xuss"
                        Movement = "Fields"
                        Weapon = "Knife missiles" }
               }

既然我们已经有了模型和存储库,我们就可以开始查看视图模型了。视图模型的主要目的之一是为视图提供一些可以数据绑定的属性。这意味着视图模型类由许多相互作用的小方法和属性组成,所以我认为向您展示整个视图模型,然后讨论组成它的各个部分会很有帮助。

    type RobotsViewModel(robotsRepository: RobotsRepository) =  
        // Backing field of the on property change event.
        let propertyChangedEvent =
            new DelegateEvent<PropertyChangedEventHandler>()

        // Our collection of robots.
        let robots =
            let allRobots = robotsRepository.GetAll()
            new ObservableCollection<Robot>(allRobots)

        // The currently selected robot
        // initialized to an empty robot.
        let mutable selectedRobot =
            {Name=""; Movement=""; Weapon= ""}

        // Default constructor, which creates a repository.
        new () = new RobotsViewModel(new RobotsRepository())

        // Implementing the INotifyPropertyChanged interface
        // so the GUI can react to events.
        interface INotifyPropertyChanged with
            [<CLIEvent>]
            member x.PropertyChanged = propertyChangedEvent.Publish

        // Helper method to raise the property changed event.
        member x.OnPropertyChanged propertyName =
            let parameters: obj[] =
                [| x; new PropertyChangedEventArgs(propertyName) |]
            propertyChangedEvent.Trigger(parameters)

        // Collection of robots that the GUI will data bind to.
        member x.Robots =
            robots

        // Currently selected robot that the GUI will data bind to.
        member x.SelectedRobot
            with get () = selectedRobot
            and set value =
                selectedRobot <- value
                x.OnPropertyChanged "SelectedRobot"

关于RobotsViewModel首先要注意的是两个构造函数。第一个构造函数接受一个RobotsRepository参数,这样类就可以访问机器人数据。这个构造函数的其余部分初始化了类的其余方法可以访问的字段。初始化这些字段后,我们定义第二个构造函数,一个不带参数的构造函数,它必须调用第一个构造函数。我们调用第一个构造函数,并将其传递给我们的存储库的一个新实例:

        // Default constructor, which creates a repository.
        new () = new RobotsViewModel(new RobotsRepository())

现在我们已经看到了这两个构造函数,让我们看看我们定义的每个字段以及它们在类中是如何使用的。首先,我们定义一个字段,为我们的事件处理程序提供后备存储。需要一个事件处理程序来实现INotifyPropertyChanged接口,这是视图模型通知视图变化的方式。创建后备存储很简单,我们只需要创建一个DelegateEvent对象并将其绑定到一个字段。

        // Backing field of the on property change event.
        let propertyChangedEvent =
            new DelegateEvent<PropertyChangedEventHandler>()

比事件后备库更有趣的是我们如何使用properyChangedEvent字段。我们以两种方式使用该字段:实现事件本身,以及创建引发事件的方法。这就是我们如何通过公开DelegateEvent.Publish属性来创建事件:

        // Implementing the INotifyPropertyChanged interface
        // so the GUI can react to events.
        interface INotifyPropertyChanged with
            [<CLIEvent>]
            member x.PropertyChanged = propertyChangedEvent.Publish

我们在这里看到,事件是作为INotifyPropertyChanged接口实现的一部分公开的,该接口只有一个成员:事件PropertyChanged。我们需要用[<CLIEvent>]属性标记PropertyChanged,这样 F# 编译器就知道它应该生成一个与其他 CLR 语言兼容的事件,比如 C#,因为 F# 有自己优化的事件系统。既然我们已经公开了事件,我们需要能够调用事件。我们通过创造一个OnPropertyChanged方法来触发事件。

        // Helper method to raise the property changed event.
        member x.OnPropertyChanged propertyName =
            let parameters: obj[] =
                [| x; new PropertyChangedEventArgs(propertyName) |]
            propertyChangedEvent.Trigger(parameters)

类的这三个成员,propertyChangedEvent字段,INotifyPropertyChanged的接口实现,OnPropertyChanged方法通常放在一个基类中,这样它们就可以在应用的所有视图模型之间共享。为了简化这个例子,我将它们与视图模型放在同一个类中,因为在这个应用中我们只有一个视图模型。接下来的两个字段与允许视图绑定到视图模型的属性相关。第一个字段robots保存所有机器人数据的集合:

        // Our collection of robots.
        let robots =
            let allRobots = robotsRepository.GetAll()
            new ObservableCollection<Robot>(allRobots)

我们使用了一个可观察的集合来表示机器人的列表。如果我们在集合中添加或删除项目,此集合类型将自动通知视图。然后,我们通过一个属性公开这个集合:

        // Collection of robots that the GUI will data bind to.
        member x.Robots =
            robots

Robots属性将绑定到图形用户界面中显示机器人列表的列表视图控件。下一个字段selectedRobot代表当前选择的机器人。

        // The currently selected robot
        // initialized to an empty robot.
        let mutable selectedRobot =
            {Name=""; Movement=""; Weapon= ""}

该字段需要可变,因为它会随着时间的推移而变化。当用户选择机器人时,该字段将被更新。之所以会发生这种更新,是因为我们将在检查视图如何实现时查看数据绑定。同样,我们使用一个字段来公开这个属性,但是这次我们需要提供一个 getter 和一个 setter:

        // Currently selected robot that the GUI will data bind to.
        member x.SelectedRobot
            with get () = selectedRobot
            and set value =
                selectedRobot <- value
                x.OnPropertyChanged "SelectedRobot"

getter 只是返回我们的字段,但是 setter 必须做两件事。它必须首先更新字段selectedRobot,然后调用OnPropertyChanged方法来引发属性更改事件,该事件将通知图形用户界面更改。

我们已经查看了构成视图模型的所有内容,现在我们将查看视图本身。由于这个视图比我们之前看到的 XAML 视图更复杂,我认为首先看一下布局的图像,然后看一下完整的 XAML 列表,最后浏览一下 XAML 的要点会有所帮助。XAML 的上市时间相当长,但不要太担心这个。大多数 XAML 只是描述了控件的位置;只有几个重要的部分我们需要更详细地研究。

首先,让我们看一下应用的截图:

图 7:机器人库存应用

这里我们看到应用由左侧的列表框和右侧显示机器人细节的一些标签组成。随着用户改变选择,机器人的细节也会改变。

完整视图列表是这样的:

    <Window 
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:ViewModel="clr-namespace:FsSuccinctly.RobotsMvvm.ViewModel;assembly=Form_MVVM"
            mc:Ignorable="d"
            Width="350"
            Height="300">

      <!-- Create and data bind the ViewModel. -->
      <Window.DataContext>
        <ViewModel:RobotsViewModel></ViewModel:RobotsViewModel>
      </Window.DataContext>

      <Grid Margin="10,0,10,10" VerticalAlignment="Stretch">

        <Grid.Resources>
          <!-- Name item template. -->
          <DataTemplate x:Key="nameItemTemplate">
            <Label Content="{Binding Path=Name}"/>
          </DataTemplate>
        </Grid.Resources>

        <Grid.ColumnDefinitions>
          <ColumnDefinition />
          <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="auto"/>
          <RowDefinition Height="auto"/>
          <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <!-- Robots list. -->
        <Label Grid.Row="0" Grid.ColumnSpan="2">
          Robots Details Viewer
        </Label>
        <Grid Margin="10" Grid.Column="0"
              Grid.Row="1" VerticalAlignment="Top">
          <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto"/>
          </Grid.RowDefinitions>

          <Border Grid.Row="1">
            <Label>Names</Label>
          </Border>

          <ListBox Name="robotsBox" Grid.Row="2"
               ItemsSource="{Binding Path=Robots}"
               ItemTemplate="{StaticResource nameItemTemplate}"
               SelectedItem="{Binding Path=SelectedRobot,Mode=TwoWay}"
               IsSynchronizedWithCurrentItem="True">
          </ListBox>

        </Grid>
        <Grid Margin="10" Grid.Column="1" Grid.Row="1"
              DataContext="{Binding SelectedRobot}" VerticalAlignment="Top">
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="57*" />
            <ColumnDefinition Width="125*" />
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto" />
          </Grid.RowDefinitions>
          <!-- Name -->
          <StackPanel Grid.Column="0" Grid.ColumnSpan="2"
                      Grid.Row="0" Orientation="Horizontal">
            <Label>Name:</Label>
            <Label Content="{Binding Path=Name}"></Label>
          </StackPanel>
          <!-- Movement -->
          <StackPanel Grid.Column="0" Grid.ColumnSpan="2"
                      Grid.Row="1" Orientation="Horizontal">
            <Label>Movement:</Label>
            <Label Content="{Binding Path=Movement}"></Label>
          </StackPanel>
          <!-- Weapon -->
          <StackPanel Grid.Column="0" Grid.ColumnSpan="2"
                      Grid.Row="2" Orientation="Horizontal">
            <Label>Weapon:</Label>
            <Label Content="{Binding Path=Weapon}"></Label>
          </StackPanel>
        </Grid>
      </Grid>
    </Window>

如你所见,XAML 观点的清单很长,但别担心,只有几个要点需要注意。首先,让我们看看视图模型是如何绑定到视图的。首先,我们需要在Window标签中有一个属性来创建一个别名,这样我们就可以访问视图模型的名称空间:

xmlns:ViewModel = " clr-namespace:fs 简洁。RobotsMvvm . ViewModel 组装=表单 _MVVM”

我们现在可以使用前缀ViewModel来访问FsSuccinctly.RobotsMvvm.ViewModel中的类,作为我们的 XAML 文档中的标签。这意味着我们可以创建一个视图模型的实例,并将其绑定到 XAML 的视图数据上下文,如下所示:

      <!-- Create and data bind the ViewModel. -->
      <Window.DataContext>
        <ViewModel:RobotsViewModel></ViewModel:RobotsViewModel>
      </Window.DataContext>

视图的下一个重要部分是显示机器人的列表框,其编码如下:

          <ListBox Name="robotsBox" Grid.Row="2"
               ItemsSource="{Binding Path=Robots}"
               SelectedItem="{Binding Path=SelectedRobot,Mode=TwoWay}"

               ItemTemplate="{StaticResource nameItemTemplate}"

               IsSynchronizedWithCurrentItem="True">
          </ListBox>

这个列表框很好地利用了 WPF 强大的数据绑定。我们将ItemsSource属性绑定到视图模型的Robot属性,这样列表框将显示机器人列表。SelectedItem物业绑定到SelectedRobot物业。这利用了 WPF 的双向绑定,这意味着当用户更新用户界面中的选定项目时,列表视图的SelectedItem也会更新,并且由于双向绑定,SelectedRobot属性也会更新。ItemTemplate属性允许您控制列表框中每个项目的呈现方式。在这种情况下,我们引用前面定义的模板:

        <Grid.Resources>
          <!-- Name item template. -->
          <DataTemplate x:Key="nameItemTemplate">
            <Label Content="{Binding Path=Name}"/>
          </DataTemplate>
        </Grid.Resources>

现在我们已经看到了列表框是如何工作的,我们只需要看看机器人的细节是如何显示的。这里我们展示了关于机器人的三个信息字段,但是显示的每个字段都有相同的实现,所以我们只需要看看一个字段是如何实现的。这三个字段显示在一个网格中;我们从视图模型中将这个Grid标签绑定到我们的SelectedRobot属性。这将使我们能够轻松访问我们希望在标签中显示的字段,这些标签显示了我们所选机器人的详细信息。下面显示了网格是如何实现的。这里需要注意的重要属性是DataContext属性:

        <Grid Margin="10" Grid.Column="1" Grid.Row="1"
              DataContext="{Binding SelectedRobot}" VerticalAlignment="Top">

现在让我们看看网格中的一个字段。这里我们看到Name字段的实现:

          <!-- Name -->
          <StackPanel Grid.Column="0" Grid.ColumnSpan="2"
                      Grid.Row="0" Orientation="Horizontal">
            <Label>Name:</Label>
            <Label Content="{Binding Path=Name}"></Label>
          </StackPanel>

我们可以看到第二个标签的Content属性是如何绑定到机器人的N ame属性的。这就是视图的所有重要细节,我们几乎完成了 XAML 表单的实现。剩下的唯一事情就是加载并显示 XAML 视图。我们可以使用本章前面定义的帮助函数来实现这一点。如您所见,加载并显示 XAML 窗口非常简单:

    // Create the window.
    let win = loadWindowFromResources "RobotsWindow.xaml"

    // Create the application object and show the window.
    let app = new Application()
    [<STAThread>]
    do app.Run win |> ignore

不需要连接任何事件或进行任何其他类型的配置。XAML 视图负责将自己绑定到驱动其余交互的视图模型。

总结

我们现在已经看到了 F# 如何以几种不同的方式用于 WPF,包括使用 WPF 强大的 MVVM 设计模式。希望这给了你一个很好的想法,如何使用 F# 来创建需要用户输入结构化数据的业务应用。我们关注的是 WPF,但是在。NET 框架。虽然我们没有看到这些库的具体例子,但它们中的大多数都与 WPF 有相似的概念。希望你会发现这一章中的一些想法对其他库有用。