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

news2024/9/22 15:40:29

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#支持面向对象,所以还是能够比较容易就实现数据绑定。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

金智教育IPO过会:计划募资约6亿元,郭超、史鸣杰为实控人

7月13日&#xff0c;深圳证券交易所披露的信息显示&#xff0c;江苏金智教育信息股份有限公司&#xff08;下称“金智教育”&#xff09;获得上市委会议通过。据贝多财经了解&#xff0c;金智教育于2022年6月30日递交上市申请材料&#xff0c;先后递交了6个版本的招股书&#x…

NDK OpenGL与OpenCV实现大眼萌特效

NDK​系列之OpenGL与OpenCV实现大眼萌特效&#xff0c;本节主要是在上一节OpenGL仿抖音极快极慢录制特效视频上增加大眼萌的特效。 OpenGL视频特效系列&#xff1a; NDK OpenGL渲染画面效果 NDK OpenGL离屏渲染与工程代码整合 NDK OpenGL仿抖音极快极慢录制特效视频 NDK O…

通讯录实现

普通版 需求 通讯录可以用来存储1000个人的信息&#xff0c;每个人的信息包括&#xff1a;姓名、性别、年龄、电话、住址 提供方法&#xff1a; 添加联系人信息删除指定联系人信息查找指定联系人信息修改指定联系人信息显示所有联系人信息清空所有联系人以名字排序所有联系…

【Linux后端服务器开发】UDP协议

目录 一、端口号 二、UDP报头格式 三、UDP的特点 四、UDP协议实现网络聊天群 一、端口号 端口号port标识了一个主机上进行通信的不同的应用程序。 0 ~ 1023&#xff1a;系统端口号&#xff0c;HTTP、FTP、SSH等这些广为使用的应用层协议&#xff0c;它们的端口号都是固定…

Windows软件开发常用技巧总结

本文总结了本人在日常工作学习中遇到的问题及其解决方法&#xff0c;没有固定的涉及领域 目的就是为了在下一次遇到类似问题的时候方便查找&#xff0c;从而快速解决问题 本文不定时更新~ 目录 Windows使用 如何实现桌面图标随意排列 文件资源管理器相关 显示隐藏文件 修改…

Linux--获取最近一次的进程退出码:echo $?

举例&#xff1a; #include <stdio.h> int main() { printf("hello world,pid: %d,ppid: %…

JavaFx 用户界面控件3——TableView

1.表格视图 TableView ableView是JavaFX提供的一个强大的控件&#xff0c;可以用于显示表格数据。它通过为TableView设定items属性&#xff08;存储行数据的ObservableList对象&#xff09;和列属性&#xff08;TableColumn对象&#xff09;来完成数据填充与展示。 以下是一个…

如何做一线leader

文章目录 道领导力五个层次关键&#xff1a;信任 处事原则 术避坑指南事急则乱员工沟通向上管理人才招聘人才培养裁人员工关怀 道 领导力 五个层次 职位 当面交代事情&#xff0c;观察眼神、语气。反复确认有没有问题&#xff0c;如果有可以及时讨论策略&#xff0c;准备资源…

IDEA连接达梦数据库

在 IntelliJ IDEA 中连接达梦数据库&#xff0c;可以按照以下步骤进行操作&#xff1a; 1. 打开 IntelliJ IDEA&#xff0c;进入项目。 2. 在顶部菜单栏选择 "View" -> "Tool Windows" -> "Database"&#xff0c;打开数据库工具窗口。 3.…

自旋锁的优势和特点

ucos为何没自旋锁&#xff1f; UC/OS是一个适用于嵌入式系统的实时操作系统&#xff0c;它的设计目标是提供一种轻量级的任务调度和同步机制。相比于一般的操作系统&#xff0c;UC/OS在实现上更加精简&#xff0c;因此并没有像Linux那样的完整的锁机制。 UC/OS提供了一些基本…

17 | 从后端到前端:微服务后,前端如何设计?

微服务架构通常采用前后端分离的设计方式。作为企业级的中台&#xff0c;在完成单体应用拆分和微服务建设后&#xff0c;前端项目团队会同时面对多个中台微服务项目团队&#xff0c;这时候的前端人员就犹如维修电工一样了。 面对如此多的微服务暴露出来的 API 服务&#xff0c…

ChatGPT与Claude对比分析

一 简介 1、ChatGPT: 访问地址&#xff1a;https://chat.openai.com/ 由OpenAI研发,2022年11月发布。基于 transformer 结构的大规模语言模型,包含1750亿参数。训练数据集主要是网页文本,聚焦于流畅的对话交互。对话风格友好,回复通顺灵活,富有创造性。存在一定的安全性问题,可…

山西电力市场日前价格预测【2023-07-17】

日前价格预测 预测明日&#xff08;2023-07-17&#xff09;山西电力市场全天平均日前电价为335.50元/MWh。其中&#xff0c;最高日前电价为377.51元/MWh&#xff0c;预计出现在06: 00。最低日前电价为271.94元/MWh&#xff0c;预计出现在13: 30。 价差方向预测 1&#xff1a;实…

力扣 45. 跳跃游戏 II

题目来源&#xff1a;https://leetcode.cn/problems/jump-game-ii/description/ C题解1&#xff1a;因为每一步都要能走到下一个更远的地方&#xff0c;就比如 [2,3,1,1,4]&#xff0c;第一步虽然可以到索引2的位置&#xff0c;但是到索引1的位置下一步可以走更远。所以需要记录…

工厂方法模式详解

文章目录 前言一、工厂方法模式的定义二、举个例子三、工厂方法模式的缺点总结 前言 工厂方法模式是应用比较广泛的一种设计模式&#xff0c;它相对于简单工厂模式进行了一些优化&#xff0c;如果再增加一个具体产品不用修改代码&#xff0c;也不会违反开闭原则。 一、工厂方法…

Spring 6【什么是Spring 6、Spring框架介绍 、Spring IoC/DI 详解 】(一)-全面详解(学习总结---从入门到深化)

目录 一、Spring 6 二、Spring框架介绍 三、Spring IoC/DI 详解 一、Spring 6 1. 为什么要学习Spring 6 Spring 作为 Java程序员必会武功&#xff0c;无论是Spring的哪个版本&#xff0c;至少需要会一版本。再此基础上不会一套 组合拳SSM或SSH&#xff0c;出门都不好意思…

DAO(Data Access Object)

ProductDAO.java Search.java (Servlet) Compile Tomcat http://localhost:8080/book/chapter15/search.jsp Insert.java Compile http://localhost:8080/book/chapter15/insert.jsp

介绍用户协议报UDP

作者&#xff1a;爱塔居 专栏&#xff1a;计算机网络 文章目录 目录 文章目录 一、UDP概述 二、UDP的首部格式 一、UDP概述 用户数据报协议UDP只是在IP的数据报服务之上增加了很少一点的功能&#xff1a;复用和分用的功能以及差错检测的功能。 重点要记住UDP的主要特点&am…

JDK 下载 华为云镜像站 地址

通常去 Oracle 官网下载 JDK&#xff0c;速度很慢而且需要账号登入 Oracle 官网下载地址 https://www.oracle.com/cn/java/technologies/downloads/archive/ JDK 下载 华为云镜像站 地址 https://repo.huaweicloud.com/java/jdk/ 我们下期见&#xff0c;拜拜&#xff01;

C++程序生成文件

文章目录 VS2019 Debug 模式VS2019 Release 模式exe连接动态库和静态库的流程 VS2019 Debug 模式 在Visual Studio的Debug模式下运行程序时&#xff0c;会生成以下文件&#xff1a; 可执行文件&#xff08;.exe&#xff09;&#xff1a;生成的可执行文件&#xff08;.exe&#…