F#奇妙游(14):F#实现WPF的绑定

WPF中的绑定

绑定在UI开发中是一个非常重要的概念,它可以让我们的UI界面和数据模型之间建立起联系,当数据模型发生变化时,UI界面也会随之变化,反之亦然。这样的好处是显而易见的,我们不需要手动去更新UI界面,而是让数据模型自己去更新UI界面,这样的代码更加简洁,更加易于维护。

在传统的UI开发中,我们需要手动去更新UI界面,这样的代码往往是重复的,而且容易出错。具体的方法是通过输入控件的时间来更新数据模型,然后再通过数据模型来更新UI界面。

在.NET平台的WinForm中,在Java的Swing中,以及传统的C++的MFC中,都是通过这种方式来更新UI界面的。

时间上比较近的UI框架,例如JavaFX、WPF、UWP等,都是通过数据绑定来更新UI界面的。

WPF中的绑定

在这里插入图片描述

WPF中的绑定是通过Binding类来实现的,Binding类的构造函数接受一个字符串参数,这个字符串参数是一个路径,它指定了数据模型中的一个属性,这个属性的值会被绑定到UI界面上。

所以绑定必然包含几个要素:

  • source,提供数据的对象
  • target,使用数据的对象
  • path,数据的路径

根据不同的绑定需求,Binding类的构造函数还接受一个可选的参数,这个参数是一个BindingMode枚举值,它指定了绑定的模式,BindingMode枚举有以下几个值:

  • OneWay:单向绑定,数据模型的属性的值会被绑定到UI界面上,但是UI界面的值不会被绑定到数据模型上。
  • TwoWay:双向绑定,数据模型的属性的值会被绑定到UI界面上,UI界面的值也会被绑定到数据模型上。
  • OneWayToSource:单向绑定,UI界面的值会被绑定到数据模型上,但是数据模型的属性的值不会被绑定到UI界面上。
  • OneTime:单次绑定,数据模型的属性的值会被绑定到UI界面上,但是这个绑定只会发生一次,之后数据模型的属性的值的变化不会被绑定到UI界面上。
  • Default:默认绑定,这个值会根据绑定的目标来确定,如果绑定的目标是UI界面,那么就是OneWay,如果绑定的目标是数据模型,那么就是OneWayToSource。

绑定源的实现

在实现绑定的底层中,实际上还是通过事件来完成的。当数据模型的属性的值发生变化时,会触发一个事件,这个事件会被绑定目标监听到,然后绑定目标就会更新UI界面。

在.NET平台中,这个事件是PropertyChanged事件,它是INotifyPropertyChanged接口的一个事件,这个接口定义了一个PropertyChanged事件,这个事件的参数是一个PropertyChangedEventArgs对象,这个对象包含了一个PropertyName属性,这个属性指定了发生变化的属性的名称。

因此,要实现一个源,就需要实现INotifyPropertyChanged接口,然后在属性的setter中触发PropertyChanged事件。

绑定目标的实现

在.NET平台中,绑定目标是一个依赖属性,它是DependencyObject类的一个属性,这个类定义了一个SetValue方法和一个GetValue方法,这两个方法分别用于设置属性的值和获取属性的值。

在WPF中,依赖属性的名称是以“Property”结尾的,例如TextBlock类的Text属性,它的依赖属性的名称是TextProperty。

F#奇妙游

理论的知识就是上面那些,下面我们用F#实现一个WPF应用程序,这个程序就是点击计数(真是无聊啊!)。

在这里插入图片描述

创建项目

首先,我们创建一个F#的WPF应用程序,这个应用程序的名称是FSharpWpfApp1。

dotnet new console -lang F# -o FSharpWpfApp1

把对应的工程文件打开,改成大概是下面的样子。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0-windows</TargetFramework>
        <UseWpf>true</UseWpf>
        <ApplicationIcon>fsharp.ico</ApplicationIcon>
    </PropertyGroup>

    <ItemGroup>
        <Compile Include="Program.fs" />
        <Content Include="fsharp.ico" />
    </ItemGroup>

</Project>

数据模型

然后,我们创建一个数据模型,这个数据模型包含一个整数属性,这个整数属性的值会被绑定到UI界面上。

type Model() =
    let mutable count = 0
    member this.Count
        with get() = count
        and set(value) = count <- value

这个模型要改成是为了支持绑定的,所以我们需要实现INotifyPropertyChanged接口。那么我们定义一个ViewModel的基类。这个基类首先定义一个propertyChanged的事件,这个在C#中是一个委托,但是在F#中是一个Event。

接下来是一点点F#魔法,就是把一个表达式转换成一个字符串,这个字符串就是属性的名称。关于表达式的内容,我还在学……

接下来是实现接口的语法,interface XXXX with XXXXX,这个语法是F#中的接口实现语法,这个语法的意思是,实现XXXX接口,然后XXXXX是接口中函数的实现。这里就把Event的Publish函数设定为对应PropertyChanged事件的触发函数。

type ViewModelBase() =
    let propertyChanged = Event<_, _>()

    let toPropName (query: Expr) =
        match query with
        | PropertyGet (_, b, _) -> b.Name
        | _ -> ""

    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member this.PropertyChanged = propertyChanged.Publish

    abstract member OnPropertyChanged: string -> unit

    default this.OnPropertyChanged(propertyName: string) =
        propertyChanged.Trigger(this, PropertyChangedEventArgs(propertyName))

    member this.OnPropertyChanged(expr: Expr) =
        let propName = toPropName expr
        this.OnPropertyChanged(propName)

实际的OnPropertyChanged函数有两个重载,一个是接受字符串参数的,一个是接受表达式参数的。这个表达式参数是用来获取属性名称的,这样就不需要输入属性名称,改为输入获取属性的表达式,下面可以看到例子。

实际的ViewModel类就是继承这个ViewModelBase类,然后实现OnPropertyChanged函数。

type MyData() =
    inherit ViewModelBase()

    let mutable count = 0
    member x.Text = $"计数: %4d{count}"

    member this.Count
        with get () = count
        and set value =
            count <- value
            this.OnPropertyChanged(<@ this.Text @>)
            this.OnPropertyChanged("Count")

    member this.Increment() = this.Count <- this.Count + 1

可以看到,这里的Count属性的setter中,我们调用了OnPropertyChanged函数。注意,我们这个Model实际上提供了两个可以绑定的属性,一个是Count,一个是Text,但是我们只在Count属性的setter中调用了OnPropertyChanged函数,这是因为Text属性的值是由Count属性的值计算出来的,所以当Count属性的值发生变化时,Text属性的值也会发生变化,所以我们只需要在Count属性的setter中调用OnPropertyChanged函数就可以了。

这里申明属性变化包含了两种方式,一种是直接传递字符串,一种是传递表达式。这两种方式都是可以的,但是传递表达式的方式更加安全,因为它可以在编译时检查属性名称是否正确。

UI界面

UI界面这里还是没有采用XAML的方法实现,而是通过直接编写代码。XAML我还没开始学。

这里的主界面采用Grid布局来实现,首先是行和列的定义,增加两行两列。注意这里对行的高度进行限定,第二行(序号1)的高度是自动,也就是按照空间的需求定义;第一行(序号0)的高度是“*”,也就是占满剩下的全部空间。

let mainContent =


    let grid = Grid()
    // define rows and columns for grid
    grid.RowDefinitions.Add(RowDefinition(Height = GridLength(1.0, GridUnitType.Star)))
    grid.RowDefinitions.Add(RowDefinition(Height = GridLength.Auto))

    grid.ColumnDefinitions.Add(ColumnDefinition())
    grid.ColumnDefinitions.Add(ColumnDefinition())

    let textBlock =
        TextBlock(
            Foreground = Brushes.Lime,
            TextAlignment = TextAlignment.Center,
            HorizontalAlignment = HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center
        )

    textBlock.SetBinding(
        TextBlock.TextProperty,
        Binding(Source = HelloText, Mode = BindingMode.OneWay, Path = PropertyPath("Text"))
    )
    |> ignore

    let vb = Viewbox(Margin = Thickness(50.0))
    vb.Child <- textBlock

    Grid.SetRow(vb, 0)
    Grid.SetColumn(vb, 0)
    Grid.SetColumnSpan(vb, 2)
    grid.Children.Add(vb) |> ignore


    // add a button to grid
    let button =
        Button(Content = "加一", FontSize = 32.0, FontWeight = FontWeights.Bold, Margin = Thickness(10.0))

    button.Click.Add(fun _ -> HelloText.Increment())

    Grid.SetRow(button, 1)
    Grid.SetColumn(button, 0)
    grid.Children.Add(button) |> ignore

    // add a button to grid
    let button2 =
        Button(Content = "清零", FontSize = 32.0, FontWeight = FontWeights.Bold, Margin = Thickness(10.0))

    button2.Click.Add(fun _ -> HelloText.Count <- 0)

    Grid.SetRow(button2, 1)
    Grid.SetColumn(button2, 1)
    grid.Children.Add(button2) |> ignore

    grid

控件的定义很常规,属性可以作为构造函数的参数传递,也可以通过属性赋值的方式传递。唯一比较麻烦的是AttachedProperty的赋值,就是类似于Grid.Row这几个,没办法通过构造函数的参数传递,只能通过Grid.SetRow这样的函数来赋值。

在这里插入图片描述

这里的TextBlock控件的Text属性是通过绑定的方式传递的,这个绑定的源是HelloText,这个HelloText是我们在后面定义的数据模型。这里的绑定的方式是通过Binding类来实现的,Binding类的构造函数接受一个字符串参数,这个字符串参数是一个路径,它指定了数据模型中的一个属性,这个属性的值会被绑定到UI界面上。

主函数

[<STAThread>]
[<EntryPoint>]
let main _ =
    let app = Application()
    let window = Window()
    
    window.Content <- mainContent

    window.SetBinding(
        Window.TitleProperty,
        Binding(Source = HelloText, Mode = BindingMode.OneWay, Path = PropertyPath("Count"))
    )
    |> ignore
    
    // move window to center of screen
    window.WindowStartupLocation <- WindowStartupLocation.CenterScreen
    // set window size
    window.Width <- 400.0
    window.Height <- 300.0

    app.Run(window) |> ignore
    0

主函数中把窗口的标题也定义了一个绑定,到HelloText的Count属性上。这样,当HelloText的Count属性的值发生变化时,窗口的标题也会发生变化。

这里隐式包含了一个转换,就是把整数转换成字符串,这个转换是通过ToString函数实现的,这个函数是Object类的一个方法,它可以把任意类型的对象转换成字符串。

全部代码

open System
open System.ComponentModel
open System.Windows
open System.Windows.Controls
open System.Windows.Data
open System.Windows.Media

open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns

type ViewModelBase() =
    let propertyChanged = Event<_, _>()

    let toPropName (query: Expr) =
        match query with
        | PropertyGet (_, b, _) -> b.Name
        | _ -> ""

    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member this.PropertyChanged = propertyChanged.Publish

    abstract member OnPropertyChanged: string -> unit

    default this.OnPropertyChanged(propertyName: string) =
        propertyChanged.Trigger(this, PropertyChangedEventArgs(propertyName))

    member this.OnPropertyChanged(expr: Expr) =
        let propName = toPropName expr
        this.OnPropertyChanged(propName)


type MyData() =
    inherit ViewModelBase()

    let mutable count = 0
    member x.Text = $"计数: %4d{count}"

    member this.Count
        with get () = count
        and set value =
            count <- value
            this.OnPropertyChanged(<@ this.Text @>)
            this.OnPropertyChanged("Count")

    member this.Increment() = this.Count <- this.Count + 1

let HelloText = MyData()

let mainContent =


    let grid = Grid()
    // define rows and columns for grid
    grid.RowDefinitions.Add(RowDefinition(Height = GridLength(1.0, GridUnitType.Star)))
    grid.RowDefinitions.Add(RowDefinition(Height = GridLength.Auto))

    grid.ColumnDefinitions.Add(ColumnDefinition())
    grid.ColumnDefinitions.Add(ColumnDefinition())

    let textBlock =
        TextBlock(
            Foreground = Brushes.Lime,
            TextAlignment = TextAlignment.Center,
            HorizontalAlignment = HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center
        )

    textBlock.SetBinding(
        TextBlock.TextProperty,
        Binding(Source = HelloText, Mode = BindingMode.OneWay, Path = PropertyPath("Text"))
    )
    |> ignore

    let vb = Viewbox(Margin = Thickness(50.0))
    vb.Child <- textBlock

    Grid.SetRow(vb, 0)
    Grid.SetColumn(vb, 0)
    Grid.SetColumnSpan(vb, 2)
    grid.Children.Add(vb) |> ignore


    // add a button to grid
    let button =
        Button(Content = "加一", FontSize = 32.0, FontWeight = FontWeights.Bold, Margin = Thickness(10.0))

    button.Click.Add(fun _ -> HelloText.Increment())

    Grid.SetRow(button, 1)
    Grid.SetColumn(button, 0)
    grid.Children.Add(button) |> ignore

    // add a button to grid
    let button2 =
        Button(Content = "清零", FontSize = 32.0, FontWeight = FontWeights.Bold, Margin = Thickness(10.0))

    button2.Click.Add(fun _ -> HelloText.Count <- 0)

    Grid.SetRow(button2, 1)
    Grid.SetColumn(button2, 1)
    grid.Children.Add(button2) |> ignore

    grid

[<STAThread>]
[<EntryPoint>]
let main _ =
    let app = Application()
    let window = Window()
    
    window.Content <- mainContent

    window.SetBinding(
        Window.TitleProperty,
        Binding(Source = HelloText, Mode = BindingMode.OneWay, Path = PropertyPath("Count"))
    )
    |> ignore
    
    // move window to center of screen
    window.WindowStartupLocation <- WindowStartupLocation.CenterScreen
    // set window size
    window.Width <- 400.0
    window.Height <- 300.0

    app.Run(window) |> ignore
    0

总结

  1. F#的WPF开发还是比较麻烦的,特别是跟XAML比,略显繁琐。
  2. 因为F#支持面向对象,所以还是能够比较容易就实现数据绑定。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/40059.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

React native 已有项目升级兼容web

基础 概念 | webpack 中文文档 | webpack 中文文档 | webpack 中文网 深入理解Webpack及Babel的使用 - 掘金 Introduction to React Native for Web // React Native for Web Webpack 是一个现代的 JavaScript 应用程序的静态模块打包工具&#xff0c;它将应用程序所依赖的各…

回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(不调用工具箱函数)

回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(不调用工具箱函数) 文章目录 回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(不调用工具箱函数)预测效果基本介绍程序设计参考资料 预测效果 基本介绍 GRU神经网络是LSTM神经网络的一种变体&#xff0c;LSTM 神经网 …

opencv 基础学习08-图像通道操作

opencv 基础学习08-图像通道操作 什么是图像通道&#xff1f;通道操作&#xff1a;**1 通过索引拆分**2 通过opencv 函数拆分通道合并 什么是图像通道&#xff1f; OpenCV的通道拆分功能可用于将多通道图像拆分成单独的通道&#xff0c;这在图像处理和计算机视觉任务中具有许多…

电子锁语音芯片方案,低功耗声音提示ic,WT588F02B-8S

随着科技的不断发展&#xff0c;电子锁已成为现代社会中&#xff0c;安全性和便利性并存的必备设备。如何为电子锁行业增添智能化、人性化的功能已成为行业内的热门话题。 在这个迅速发展的市场中&#xff0c;深圳唯创知音推出了一款语音交互方案——WT588F02B-8S 低功耗声音提…

【云原生】Docker的初步认识,安装与基本操作

一、Docker的相关知识 Docker是一个开源的应用容器引擎&#xff0c;基于go语言开发并遵循了apache2.0协议开源。 Docker是在Linux容器里运行应用的开源工具&#xff0c;是一种轻量级的“虚拟机”。 Docker 的容器技术可以在一台主机上轻松为任何应用创建一个轻量级的、可移植的…

时序预测 | MATLAB实现Hamilton滤波AR时间序列预测

时序预测 | MATLAB实现Hamilton滤波AR时间序列预测 目录 时序预测 | MATLAB实现Hamilton滤波AR时间序列预测预测效果基本介绍程序设计参考资料预测效果 基本介绍 预测在很大程度上取决于适合周期的模型和所采用的预测方法,就像它们依赖于过滤器提取的周期一样。标准 Hodrick-P…

Bard:Google AI开始支持中文对话和看图说话了

说起时下火爆的生成式AI&#xff0c;并不是只有ChatGPT。Bard也是一个很优秀的产品&#xff0c;并且刚刚发布的很多有趣的新功能。文末告诉你如何访问Bard。 Google AI在最近的更新中发布了Bard&#xff0c;一个新的语言模型。Bard支持多种语言&#xff0c;包括中文&#xff0…

华为模拟器eNSP过程中所遇问题(40错误)与解决办法

1. 版本 2.打开ensp开启AR2204&#xff0c;报错40 3.弹出文档&#xff0c;挨着试一遍先 安装eNSP的PC上是否存在名为“VirtualBox Host-Only Network”的虚拟网卡 需要启用。虚拟网卡的设置是否符合以下要求&#xff1a;IP地址为192.168.56.1&#xff0c;子网掩码为255.255.2…

LCD-STM32液晶显示中英文-(6.unicode字符集)

目录 Unicode字符集和编码 UTF-32 UTF-16 UTF-8&#xff08;重点&#xff1a;必须掌握&#xff09; BOM ANSI Unicode字符集和编码 由于各个国家或地区都根据使用自己的文字系统制定标准&#xff0c;同一个编码在不同的标准里表示不一样的字符&#xff0c;各个标准互不兼容…

诚迈科技子公司智达诚远精耕智能驾驶,为商用落地注入创新力量

近期&#xff0c;工业和信息化部副部长辛国斌在新闻发布会上表示&#xff0c;将启动智能网联汽车准入和上路通行试点&#xff0c;组织开展城市级“车路云一体化”示范应用&#xff0c;将支持L3级及更高级别的自动驾驶功能商业化应用。根据工信部最新消息&#xff0c;《智能网联…

<C语言> 自定义类型

1.结构体 结构体是一种用户自定义的数据类型&#xff0c;允许将不同类型的数据项组合在一起&#xff0c;形成一个更大的数据结构。结构体可以包含多个成员变量&#xff0c;每个成员变量可以是不同的数据类型&#xff0c;如整数、字符、浮点数等&#xff0c;甚至可以包含其他结构…

数据可视化——根据提供的数据,将数据经过处理后以折线图的形式展现

文章目录 前言处理数据获取数据筛选数据将JSON数据转换为Python数据筛选出横坐标数据和纵坐标数据 根据处理后的数据绘制折线图整体代码展示 前言 前面我们学习了如何使用 pyecharts 模块绘制简单的折线图&#xff0c;那么今天我将为大家分享&#xff0c;如何根据提供的数据将…

Python+Qt窗体或Django网页支付宝收款码-扫码付款实例

程序示例精选 PythonQt窗体或Django网页支付宝收款码-扫码付款实例 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<PythonQt窗体或Django网页支付宝收款码-扫码付款实例>>编写代…

Nautlius Chain主网正式上线,模块Layer3时代正式开启

Nautilus Chain 是在 Vitalik Buterin 提出 Layer3 理念后&#xff0c; 对 Layer3 领域的全新探索。作为行业内首个模块化 Layer3 链&#xff0c;我们正在对 Layer3 架构进行早期的定义&#xff0c;并有望进一步打破公链赛道未来长期的发展格局。 在今年年初&#xff0c;经过我…

【7天学GO】第1章 开发环境

1.1 开篇介绍(必看) A. Why choose the go language B. 学语言阶段 1.2 环境搭建前戏 A. 学习一门语言步骤 B. 编译型与解释型 1.3 mac系统Go开发环境搭建 (略) 1.4 linux系统Go开发环境搭建 (略) 1.5 windows系统Go开发环境搭建 A. 开发环境搭建 Stage 1&#xff1a…

Spring整合Junit

Spring整合Junit 在之前文章中Spring的测试方法几乎都能够看到如下的代码&#xff1a; ApplicationContext context new ClassPathXmlApplicationContext("xxx.xml"); XXX xxx context.getBean(XXX.class);它的作用是创建Spring容器&#xff0c;最终获取到对象&…

【前端知识】React 基础巩固(二十六)——Portals 的使用

React 基础巩固(二十六)——Portals 的使用 Portals 通常&#xff0c;组件会渲染到 root 节点下。可使用 Portals 将组件渲染至其他节点。 添加 id 为 more、modal 的 div 元素 <div id"root"></div> <div id"more"></div> &l…

第六章:U-Net——医学图像分割的卷积神经网络

0.摘要 大多数人都认为成功训练深度网络需要成千上万个注释训练样本。在本文中&#xff0c;我们提出了一种网络和训练策略&#xff0c;依靠强大的数据增强来更有效地利用现有的注释样本。该架构由一个收缩路径和一个对称扩展路径组成&#xff0c;收缩路径用于捕捉上下文…

如何下载SRA存放在AWS的原始数据

通常&#xff0c;我们都是利用prefetch从NCBI上获取数据&#xff0c;然后用fasterp-dump/fastq-dump 转成fastq。但遗憾的SRA的数据是原数据的有损压缩&#xff0c;比如说我19年参与发表的文章里单细胞数据上传的是3个文件&#xff0c;但是当时的faster-dump/fastq-dump只能拆出…

Qt与Web混合开发:实现双向通信

引言 在当今的软件开发中&#xff0c;将Qt和Web技术结合起来进行混合开发变得越来越流行。Qt作为强大的C框架&#xff0c;提供了丰富的图形界面和功能库&#xff0c;而Web技术则提供了灵活性和跨平台的优势。结合这两种技术&#xff0c;我们可以开发出功能强大、具有吸引力的应…