首页 > 解决方案 > 如何在 F# Elmish.WPF 中设置 ViewModel 类型以供 WPF DataTemplate 使用?

问题描述

我正在学习使用 Elmish.WPF。下面是带有 UserControl ContactDetailsView 的简单示例的选项卡控件的典型 XAML 代码:

XMAL:

<Window x:Class="FrontOfficeV.MainWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             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:local="clr-namespace:FrontOfficeV"
             xmlns:vm="clr-namespace:Models;assembly=Models"
             xmlns:vi="clr-namespace:Views;assembly=Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
       
        <TabControl Grid.Row="1" ItemsSource="{Binding Details}">
            <TabControl.ItemContainerStyle>
                <Style TargetType="{x:Type TabItem}">
                    <Setter Property="Header" Value="{Binding Name}" />
                </Style>
            </TabControl.ItemContainerStyle>
            <TabControl.Resources>
                <DataTemplate DataType="{x:Type vm:ContactDetail}">
                    <vi:ContactDetailsView />
                </DataTemplate>
                <DataTemplate DataType="{x:Type vm:Internet}">
                    <vi:InternetView/>
                </DataTemplate>
                <DataTemplate DataType="{x:Type vm:PhoneNumber}">
                    <vi:PhoneNumbersView/>
                </DataTemplate>
                <DataTemplate DataType="{x:Type vm:Address}">
                    <vi:AddressesView/>
                </DataTemplate>
            </TabControl.Resources>
        </TabControl>

    </Grid>
</Window>

<UserControl x:Class="Views.ContactDetailsView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             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:local="clr-namespace:Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <TextBlock Text="{Binding Content}" />
    </Grid>
</UserControl>

这是我用来向 xaml 提供视图的 F# 代码:

namespace Models

open Elmish.WPF
open Elmish
open System
open System.Windows

type ContactDetail = { Name: string; Content: string; Text: string }
type Internet      = { Name: string; Content: string; Text: string }
type PhoneNumber   = { Name: string; Content: string; Text: string }
type Address       = { Name: string; Content: string; Text: string }

    module FrontOffice =    
        type Details =
            | ContactDetail of ContactDetail * Id: Guid
            | Internet      of Internet   * Id: Guid
            | PhoneNumber   of PhoneNumber   * Id: Guid
            | Address       of Address  * Id: Guid
            
            member this.id = 
                   match this with
                   | ContactDetail(_, id)
                   | Internet(_, id)
                   | PhoneNumber(_, id)
                   | Address(_, id) -> id

            member this.name = 
                match this with
                | ContactDetail(cd,_) -> cd.Name
                | Internet(i,_)  -> i.Name
                | PhoneNumber(pn,_) -> pn.Name
                | Address(ad,_) -> ad.Name 

            member this.content = 
                match this with
                | ContactDetail(cd,_) -> cd.Content
                | Internet(i,_)  -> i.Content
                | PhoneNumber(pn,_) -> pn.Content
                | Address(ad,_) -> ad.Content 
    
        let contactDetail  : ContactDetail = { Name="Contact Detail"; Content="Content for Contact Detail"; Text="here is the contact detail text" }    
        let internet       : Internet = { Name="Internet";       Content="Content for Internet";       Text="here is the internet text" }
        let phoneNumber    : PhoneNumber =  {Name="Phone Number";   Content="Content for phone number";   Text="here is the phone number text" }
        let address        : Address = { Name="Address";        Content="Content for Address";        Text="here is the Address text" }
       
        let details   = [ContactDetail (contactDetail,Guid.NewGuid())
                         Internet      (internet,Guid.NewGuid())
                         PhoneNumber   (phoneNumber,Guid.NewGuid())
                         Address       (address,Guid.NewGuid())
                         ]


        /// This is the main data model for our application
        type Model = {
          ClickCount: int
          Message: string
          Details: Details list
        }

        /// This is used to define the initial state of our application. It can take any arguments, but we'll just use unit. We'll need the Cmd type.
        /// Notice that we return a tuple. The first field of the tuple tells the program the initial state. The second field holds the command to issue.
        /// This is the standard Elmish init() (not special to Elmish.WPF).
        let init() = 
           {
              ClickCount = 0
              Message = "Hello Elmish.WPF"
              Details = details
           }
    
        /// This is a discriminated union of the available messages from the user interface
        type Msg =
          | ButtonClicked
          | Reset

        /// This is the Reducer Elmish.WPF calls to generate a new model based on a message and an old model.
        /// The update function will receive the change required by Msg, and the current state. It will produce a new state and potentially new command(s).
        let update (msg: Msg) (model: Model) =
          match msg with 
          | ButtonClicked -> {model with ClickCount = model.ClickCount + 1}
          | Reset -> init()

     
        /// Elmish.WPF uses this to provide the data context for your view based on a model.
        /// The bindings is the view for Elmish.WPF
        /// Define the “view” function using the Bindings module. This is the central public API of Elmish.WPF. Normally in Elm/Elmish this 
        /// function is called view and would take a model and a dispatch function (to dispatch new messages to the update loop) and return 
        /// the UI (e.g. a HTML DOM to be rendered), but in Elmish.WPF this function is in general only run once and simply sets up bindings 
        /// that XAML-defined views can use. Therefore, it is called bindings instead of view.
        let bindings(): Binding<Model, Msg> list =
            [
              // One-Way Bindings
              "ClickCount" |> Binding.oneWay (fun m -> m.ClickCount)
              "Message" |> Binding.oneWay (fun m -> m.Message)
              "Details" |> Binding.subModelSeq((fun m -> m.Details), (fun detail -> detail.id), fun () ->
                  [
                      "Id"   |> Binding.oneWay (fun (_, detail) -> detail.id)
                      "Name" |> Binding.oneWay (fun (_, detail) -> detail.name)
                      "Content" |> Binding.oneWay (fun (_,detail) -> detail.content)
                  ])

              
              // Commands
              "ClickCommand" |> Binding.cmd ButtonClicked
              "ResetCommand" |> Binding.cmd Reset
            ]

        /// This is the application's entry point. It hands things off to Elmish.WPF     
        let entryPoint (mainWindow: Window) =    
            Program.mkSimpleWpf init update bindings
            |> Program.runWindowWithConfig
                       { ElmConfig.Default with LogTrace = true; Measure = true; MeasureLimitMs = 1 }
                       mainWindow
    

一切都编译并运行!

但是,在检查选项卡内容时,WPF 会报告:

Elmish.WPF.ViewModel'2[System.Object.System.Object]

所以我相信 XAML 中的 DataTemplate 无法匹配来自 Elmish.WPF 的 DataType。如何解决这个问题?我错过了什么?(我猜我需要以某种方式将 Elmish.WPF.ViewModel'2[System.Object.System.Object] 设置为适当的 DataType ???)

TIA

标签: f#datatemplateelmish-wpf

解决方案


推荐阅读