wenmo8 发布的文章

Dify简介

Dify是一个开源的大语言模型(Large Language Model, LLM)应用开发平台。它融合了后端即服务(Backend as a Service, BaaS)和LLMOps的理念,旨在帮助开发者,甚至是非技术人员,能够快速搭建和部署生成式AI应用程序。

Dify的主要特点包括:

  1. 简化开发流程
    :通过提供一系列工具和服务来简化大语言模型应用的开发流程,使得即使是不具备深厚技术背景的个人也能构建复杂的AI应用。
  2. 支持多种模型
    :Dify支持多种大型语言模型,比如GPT系列模型等,这为用户提供了灵活的选择,可以根据具体需求选择最适合的模型。
  3. LLMOps支持
    :LLMOps是指针对大型语言模型的开发、部署、维护和优化的一整套实践和流程。Dify提供了LLMOps的支持,帮助用户更高效地管理和利用这些模型。
  4. 社区与资源
    :作为一个开源项目,Dify拥有活跃的技术社区,提供了丰富的学习资源和技术支持,便于用户学习和交流经验。
    总之,Dify的目标是降低创建生成式AI应用程序的技术门槛,使得更多人能够参与到这一领域的创新中来。无论是个人开发者还是企业团队,都可以借助Dify快速实现从想法到产品的转化。

开源地址:

开源地址:
https://github.com/langgenius/dify

Dify安装(本文Centos)

克隆 Dify 代码到本地
git clone https://github.com/langgenius/dify.git

然后
进入到源代码中的 docker 目录下,一键启动

cd dify/docker
cp .env.example .env
docker compose up -d

注意在下载镜像过程中可能会网络超时的情况:

作者多次失败,解决办法如下:

编辑sudo vim /etc/docker/daemon.json

{

"registry-mirrors": [
"
https://docker.1panel.live
",
"
https://docker.nju.edu.cn
",
"
https://docker.m.daocloud.io
",
"
https://dockerproxy.com
",
"
http://hub-mirror.c.163.com
",
"
https://docker.mirrors.ustc.edu.cn
",
"
https://registry.docker-cn.com
"
]
}

重启 Docker 服务

# 重启 Docker 服务
sudo systemctl daemon-reload
sudo systemctl restart docker

重新下载镜像和启动容器

docker compose up -d

Dify访问(本文Centos)

访问地址:
http://192.168.0.100

首次设置管理员账号和密码

主界面:



后续部分,我们将深入探讨Dify的实际应用案例,展示如何利用这一平台来构建和优化生成式AI应用。通过具体的项目实例,我们将演示从概念设计到实际部署的全过程,包括如何选择合适的语言模型、集成第三方服务以及调整模型参数以适应特定业务场景。此外,我们还将分享一些最佳实践,帮助读者理解如何高效地使用Dify来解决现实世界中的挑战。


自从互联网普及之后,用于视频直播的流媒体技术就发展起来。这几十年中,比较有影响的主要有MMS、RTSP、RTMP、HLS、SRT、RIST几种,分别介绍如下。

1、MMS协议

MMS全称Microsoft Multimedia Server,意思是微软多媒体服务器,它是微软公司在上世纪九十年代发布的多媒体服务器解决方案,可用于传输微软音视频格式的流媒体直播数据。
MMS协议的直播地址形如mms://***,可通过MMS传输的视频格式为WMV,音频格式为WMA,音视频数据封装之后的文件格式为ASF。
MMS协议内部又分为MMSU和MMST,其中MMSU表示MMS协议结合UDP数据传送。如果MMSU连接失败,服务器会尝试使用MMST,这个MMST表示MMS协议结合TCP数据传送。
因为MMS协议由微软公司提出,不兼容其他格式的音视频数据流,所以随着WMV/WMA标准的式微,MMS协议也逐渐无人问津了。

2、RTSP协议

RTSP全称Real Time Streaming Protocol,意思是实时流传输协议,它是网景公司和RealNetworks公司在上世纪九十年代联合提出的多媒体实时传输协议。
RTSP协议的直播地址形如rtsp://***,早期可通过RTSP传输的视频格式为RM,音频格式为RA,音视频数据封装之后的文件格式为RM或RMVB。后来RTSP协议增加支持MPEG的音视频标准,即支持传输视频格式H.264,音频格式AAC。
RTSP协议的安全版本是RTSPS,也就是给RTSP协议增加了TLS/SSL支持。RTSPS使用了TLS/SSL协议来加密和保护数据传输,以防止数据在传输过程中被窃听和篡改。
因为RTSP提出较早,对服务端的复杂度要求比较高,以至流媒体服务器SRS干脆放弃支持RTSP协议,直播录制软件OBS Studio也没支持该协议。在流媒体服务器中,EasyDarwin、MediaMTX、ZLMediaKit支持RTSP协议。手机直播软件则有RTMP Streamer支持RTSP协议。

3、RTMP协议

RTMP全称Real Time Messaging Protocol,意思是实时消息传输协议,它是Adobe公司在零零年代提出的流媒体数据传输协议。
RTMP协议的直播地址形如rtmp://***,可通过RTMP传输的视频格式为H.264,音频格式为MP3或者AAC,音视频数据封装之后的文件格式为FLV或F4V。
RTMP协议的安全版本是RTMPS,也就是给RTMP协议增加了TLS/SSL支持。RTMPS采用安全套接字层 (SSL) 和传输层安全性 (TLS) 两种加密协议,使数据传输更加安全。
RTMP提出时间较早,最后一次更新时间在2012年,以至于未能支持HEVC和AV1等后期的音视频编码标准。又因为FLV格式没落已久,以至HTML5规范干脆移除了Flash插件,导致如今浏览器都不支持rtmp链接,连FFmpeg也迟至6.1版才给rtmp协议支持hevc格式。
不过好在RTMP的稳定性高,服务端的实现相对容易,并且之前的移动互联网爆发迅速,新的流媒体协议未能及时推出,使得RTMP协议被大量应用于网络直播领域。
在流媒体服务器中,MediaMTX、ZLMediaKit、SRS都支持RTMP协议。在直播软件中,电脑端的OBS Studio支持RTMP协议,手机端的RTMP Streamer和SRT Streamer都支持RTMP协议。
通过RTMP协议实现直播功能的说明参见之前的文章《利用RTMP协议构建电脑与手机的直播Demo》和《使用RTMP Streamer开启APP直播推流》。

4、HLS协议

HLS全称HTTP Live Streaming,意思是基于HTTP的流媒体传输协议,它是苹果公司于2009年提出的一种由于传输音视频的协议交互方式。
HLS采用HTTP协议传输音视频数据,访问地址形如“http://***.m3u8”。HLS协议通过将音视频流切割成TS切片及生成m3u8的播放列表文件,并通知客户端通过HTTP协议下载播放列表文件,按照列表文件中的顺序下载切片文件并播放,从而实现边下载边播放,类似于实时在线播放的效果。
由于HLS在传输层只采用HTTP协议,因此它具备HTTP协议的网络优势,比如很方便透过防火墙或者代理服务器,可简单的实现媒体流的负载均衡。因为HLS协议把视频流分片传输,使得在直播时延时较大,所以HLS更多用于视频点播领域。
关于HLS协议的更多说明参见之前的文章《分析SRS对HLS协议里TS包的插帧操作》和《解析H.264码流中的SPS帧和PPS帧》。

5、SRT协议

SRT全称Secure Reliable Transport,意思是安全可靠传输协议,它由由Haivision 和 Wowza共同创建的SRT联盟提出。
SRT协议协议的直播地址形如srt://***,它引入了AES加密算法,无需像RTSP和RTMP那样引入专门的SSL证书。作为较新的流媒体协议,SRT支持更多的音视频封装格式。只是该协议的支持库libsrt在2017年才开源,因此未能在移动互联网时代大量铺开,目前主要应用于大型电视直播领域。
FFmpeg从4.0开始支持集成第三方的libsrt库。在流媒体服务器中,MediaMTX、ZLMediaKit、SRS都支持SRT协议。在直播软件中,电脑端的OBS Studio从在25.0开始支持SRT协议,手机端的和SRT Streamer支持SRT协议,而RTMP Streamer不支持SRT协议,只有其升级版才支持SRT协议。
通过SRT协议实现直播功能的说明参见之前的文章《利用SRT协议构建手机APP的直播Demo》和《使用SRT Streamer开启APP直播推流》。

6、RIST协议

RTST全称Reliable Internet Stream Transport,意思是可信赖的互联网流媒体协议,它由2017年成立的RIST工作组提出。
RIST是一个在传输层使用UDP协议,并在应用层提供可靠性和流控制功能的流传输协议。它并不是一个纯粹的应用层协议,而是在传输层和应用层之间操作的协议。RIST和SRT具有相同的加密级别,都支持大容量流媒体和前向纠错功能。
RIST协议的制定时间比SRT还晚,虽然晚制定会多考虑新功能,比如RIST支持点到多点广播,而SRT不支持;但是晚制定拖累了各开源软件对RIST的支持力度,比如OBS Studio早在25.0开始支持SRT,迟至27.0才开始支持RIST,另一个直播录制软件RootEncoder已支持SRT尚未支持RIST,流媒体服务器MediaMTX已支持SRT尚未支持RIST。
FFmpeg从4.4开始支持集成第三方的librist库。在流媒体服务器中,MediaMTX、ZLMediaKit、SRS都不支持RIST协议。在直播软件中,电脑端的OBS Studio从在27.0开始支持SRT协议,手机端尚未有开源软件支持RIST协议。

总的来说,目前国内占据主要市场份额的直播协议仍是RTMP,不过拥有更好性能的SRT协议正在逐步迎头赶上,比如腾讯视频云、京东视频云等等就引入了SRT协议。有关直播系统的搭建说明参见之前的文章《从0开始搭建直播系统的开源软件架构》。

更多详细的FFmpeg开发知识参见
《FFmpeg开发实战:从零基础到短视频上线》
一书。

编译工具链

  • 我们写程序的时候用的都是集成开发环境 (IDE: Integrated Development Environment),集成开发环境可以极大地方便我们程序员编写程序,但是配置起来也相对麻烦。在 Linux 环境下,我们用的是编译工具链,又叫软件开发工具包(SDK:Software Development Kit)。Linux 环境下常见的编译工具链有:GCC 和 Clang,我们使用的是 GCC。

编译

准备工作

  • 查看当前系统是否安装gcc,g++,gdb。
    gcc --version
    g++ --version
    gdb --version
    image
  • 未安装可通过命令安装。
    sudo apt update
    sudo apt install gcc g++ gdb

生成可执行程序/编译过程

image

gcc -E hello.c -o hello.i # -E激活预处理,生成预处理后的文件
gcc -S hello.i -o hello.s # —S激活预处理和编译,生成汇编代码
gcc -c hello.s -o hello.o # -c激活预处理、编译和汇编,生成目标文件
gcc hello.o -o hello # 执行所有阶段,生成可执行程序

gcc -c hello.c # 生成目标文件,gcc会根据文件名hello.c生成hello.o
gcc hello.o -o hello # 生成可执行程序hello,这里我们需要指定可执行程序的名称,否则会默认生成a.out
gcc hello.c -o hello # 编译链接,生成可执行程序hello

image
image

gcc与g++区别

  • gcc

    g++
    都是
    GNU(组织)
    的一个编译器
  • 误区一

    gcc
    只能编译 c 代码,g++ 只能编译 c++ 代码
    • 后缀为
      .c
      的,
      gcc
      把它当作是 C 程序,而
      g++
      当作是
      c++
      程序
    • 后缀为
      .cpp
      的,两者都会认为是
      C++
      程序,
      C++
      的语法规则更加严谨一些
    • 编译阶段,
      g++
      会调用
      gcc
      ,对于
      C++
      代码,两者是等价的,但是因为
      gcc
      命令不能自动和
      C++
      程序使用的库联接,所以通常用
      g++
      来完成链接,为了统一起见,干脆编译/链接统统用
      g++
      了,这就给人一种错觉,好像
      cpp
      程序只能用
      g++
      似的
  • 误区二

    gcc
    不会定义
    __cplusplus
    宏,而
    g++

    • 实际上,这个宏只是标志着编译器将会把代码按 C 还是 C++ 语法来解释
    • 如上所述,如果后缀为
      .c
      ,并且采用
      gcc
      编译器,则该宏就是未定义的,否则,就是已定义
  • 误区三
    :编译只能用
    gcc
    ,链接只能用
    g++
    • 严格来说,这句话不算错误,但是它混淆了概念,应该这样说:编译可以用
      gcc/g++
      ,而链接可以用
      g++
      或者
      gcc -lstdc++
    • gcc
      命令不能自动和C++程序使用的库联接,所以通常使用
      g++
      来完成链接。但在编译阶段,
      g++
      会自动调用
      gcc
      ,二者等价

条件编译

预处理指令

1) #if [#elif] [#else] #endif
2) #ifdef [#elif] [#else] #endif
3) #ifndef [#elif] [#else] #endif

指令格式

1. #if 指令的格式

#if 常量表达式
...
#endif

当预处理器遇到 #if 指令时,会计算后面常量表达式的值。如果表达式的值为 0,则#if 与 #endif 之间的代码会在预处理阶段删除;否则,#if 与 #endif 之间的代码会被保留,交由编译器处理。
#if 指令常用于调试程序,如下所示:

#define DEBUG 1
...
#if DEBUG
	printf("i = %d\n", i);
	printf("j = %d\n", j);
#endif

2. defined运算符

是预处理器的一个运算符,它后面接标识符。如果标识符是一个定义过的宏则值为 1,否则值为 0。defined 运算符常和 #if 指令一起使用,比如:

#if defined(DEBUG)
...
#endif

仅当 DEBUG 被定义成宏时,#if 和 #endif 之间的代码会保留到程序中。defined 后面的括号不是必须的,因此可以写成这样:
#if defined DEBUG
defined 运算符仅检测 DEBUG 是否有被定义成宏,所以我们不需要给 DEBUG 赋值:
#define DEBUG

3. #ifdef 的格式

#ifdef 标识符
...
#endif

当标识符有被定义成宏时,保留 #ifdef 与 #endif 之间的代码;否则,在预处理阶段删除 #ifdef 与 #endif 之间的代码。等价于:

#if defined(标识符)
...
#endif

4. #ifndef 的格式

#ifndef 标识符
...
#endif

它的作用恰恰与 #ifdef 相反:当标识符没有被定义成宏时,保留 #ifndef 与 #endif之间的代码。

作用

1. 编写可移植的程序

下面的例子会根据 WIN32、MAC_OS 或 LINUX 是否被定义为宏,而将对应的代码包含到程序中:

#if defined(WIN32)
...
#elif defined(MAC_OS)
...
#elif defined(LINUX)
...
#endif

我们可以在程序的开头,定义这三个宏中的一个,从而选择一个特定的操作系统

2. 为宏提供默认定义

我们可以检测一个宏是否被定义了,如果没有,则提供一个默认的定义:

#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endif

3. 避免头文件重复包含

多次包含同一个头文件,可能会导致编译错误(比如,头文件中包含类型的定义)。因此,我们应该避免重复包含头文件。使用 #ifndef 和 #define 可以轻松实现这一点:

#ifndef __WD_FOO_H
#define __WD_FOO_H
typedef struct {
    int id;
    char name[25];
    char gender;
    int chinese;
    int math;
    int english;
} Student;
#endif

4. 临时屏蔽包含注释的代码

我们不能用 /
...
/ "注释掉" 已经包含 /
...
/注释的代码,即
不能嵌套多行注释
。但是我们可以用 #if 指令来实现:

#if 0
包含/*...*/注释的代码
#endif

注:这种屏蔽方式,我们称之为
"条件屏蔽"

库的链接

  • 库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些
    可以直接拿来用的变量、函数或类
  • 库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是
    库不能单独运行
  • 库文件有两种,
    静态库

    动态库(共享库)
    。区别是:
    • 静态库
      在程序的链接阶段被复制到了程序中
    • 动态库
      在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用
  • 库的好处:
    代码保密

    方便部署和分发

静态库的制作

  • 规则

image

  • 示例:有如下图所示文件(其中每个分文件用于实现四则运算),将其打包为
    静态库
add.c源代码
#include <stdio.h>

int add(int a, int b)
{
    return a+b;
}
sub.c源代码
#include <stdio.h>

int sub(int a, int b)
{
    return a-b;
}
mul.c源代码
#include <stdio.h>

int mul(int a, int b)
{
    return a*b;
}
div.c源代码
#include <stdio.h>

double div(int a, int b)
{
    return (double)a/b;
}
head.h头文件
#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 减法
int sub(int a, int b);
// 乘法
int mul(int a, int b);
// 除法
double div(int a, int b);
#endif
main.c源文件
#include <stdio.h>
#include "head.h"

int main()
{
    int a = 20;
    int b = 12;
    printf("a = %d, b = %d\n", a, b);
    printf("a + b = %d\n", add(a, b));
    printf("a - b = %d\n", subtract(a, b));
    printf("a * b = %d\n", multiply(a, b));
    printf("a / b = %f\n", divide(a, b));
    return 0;
}
  • 查看目录结构
    tree
    image
  1. 生成
    .o
    文件:
    gcc -c 文件名
    image


  2. .o
    文件打包:
    ar rcs libxxx.a xx1.o xx2.o
    image

    image

    image

静态库的使用

  • 需要提供
    静态库文件和相应的头文件

  • 编译运行:
    gcc main.c -o app -I ./include -l calc -L ./lib


    • -I ./include
      :指定头文件目录,如果不指定,出现编译错误

      image

    • -l calc
      :指定静态库名称,如果不指定,出现链接错误

      image

    • -L ./lib
      :指定静态库位置,如果不指定,出现链接错误

      image

    • 正确执行
      (成功生成
      app
      可执行文件)

      image

    • 测试程序

      image

动态库的制作

  • 规则
    image

  • 示例:有如下图所示文件(其中每个分文件用于实现四则运算),将其打包为
    动态库

    image


    1. 生成
      .o
      文件:
      gcc -c -fpic 文件名

      image


    2. .o
      文件打包:
      gcc -shared xx1.o xx2.o -o libxxx.so

      image

动态库的使用

  • 需要提供
    动态库文件和相应的头文件

  • 定位动态库(
    原因见工作原理->如何定位共享库文件
    ,其中路径为动态库所在位置)


    • 方法一:修改环境变量,
      当前终端生效
      ,退出当前终端失效

      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/u/Desktop/Linux/calc/lib
      
    • 方法二:修改环境变量,用户级别永久配置

      # 修改~/.bashrc
      vim ~/.bashrc
      
      # 在~/.bashrc中添加下行,保存退出
      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/u/Desktop/Linux/calc/lib
      
      # 使修改生效
      source ~/.bashrc
      
    • 方法三:修改环境变量,系统级别永久配置

      # 修改/etc/profile
      sudo vim /etc/profile
      
      # 在~/.bashrc中添加下行,保存退出
      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/u/Desktop/Linux/calc/lib
      
      # 使修改生效
      source /etc/profile
      
    • 方法四:修改
      /etc/ld.so.cache文件列表

      # 修改/etc/ld.so.conf
      sudo vim /etc/ld.so.conf
      
      # 在/etc/ld.so.conf中添加下行,保存退出
      /home/u/Desktop/Linux/calc/lib
      
      # 更新配置
      sudo ldconfig
      
  • 有如下结构文件,其中
    main.c
    测试文件

    image

  • 配置环境变量
    image

  • 编译运行:
    gcc main.c -o app -I ./include -l calc -L ./lib

    image

  • 测试程序

    image

  • 如果不将动态库文件绝对路径加入环境变量,则会出现以下错误

    image

工作原理

  • 静态库:
    GCC
    进行链接时,会把静态库中代码打包到可执行程序中

  • 动态库:
    GCC
    进行链接时,动态库的代码不会被打包到可执行程序中

  • 程序启动之后,动态库会被动态加载到内存中,通过
    ldd (list dynamic dependencies)
    命令检查动态库依赖关系

    image

  • 如何定位共享库文件呢?


    • 当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道
      绝对路径
      。此时就需要系统的动态载入器来获取该绝对路径
    • 对于
      elf格式
      的可执行程序,是由
      ld-linux.so
      来完成的,它先后搜索
      elf文件

      DT_RPATH
      段 =>
      环境变量LD_LIBRARY_PATH
      =>
      /etc/ld.so.cache文件列表
      =>
      /lib/

      usr/lib
      目录找到库文件后将其载入内存

静态库和动态库的对比

程序编译成可执行程序的过程

image

静态库制作过程

image

动态库制作过程

image

静态库的优缺点

image

动态库的优缺点

image

在数据绑定过程中,我们经常会使用
StringFormat
对要显示的数据进行格式化,以便获得更为直观的展示效果,但在某些情况下格式化操作并未生效,例如
Button

Content
属性以及
ToolTip
属性绑定数据进行
StringFormat
时是无效的。首先回顾一下
StringFormat
的基本用法。

StringFormat
的用法

StringFormat

BindingBase
的属性,指定如果绑定值显示为字符串,应如何设置该绑定的格式。因此,
BindingBase
的三个子类:
Binding

MultiBinding

PriorityBinding
都可以对绑定数据进行格式化。

Binding

Binding
是最常用的绑定方式,使用
StringFormat
遵循
.Net格式字符串标准
即可。例如:

<TextBlock Text="{Binding Price,ElementName=self,StringFormat={}{0:C}}"/>

或者

<TextBlock Text="{Binding TestString,ElementName=self,StringFormat=test:{0}}"/>

其中
{0}
表示第一个数值,如果
StringFormat
属性的值是以花括号开头,前边需要有一对花括号
{}
进行转义,也就是第一个例子中的
{}{0:C}
,否则不需要,如第二个示例一样。
如果设置
Converter

StringFormat
属性,则首先将转换器应用于数据值,然后
StringFormat
应用该值。

MultiBinding

Binding
绑定时,格式化只能指定一个参数,
MultiBinding
绑定时则可指定多个参数。例如:

<TextBlock>
    <TextBlock.Text>
        <MultiBinding StringFormat="{}{0} {1}">
            <Binding Path="FirstName" ElementName="self"/>
            <Binding Path="LastName" ElementName="self"/>
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

这个例子中
MultiBinding
是由多个子
Binding
组成,
StringFormat
仅在设置
MultiBinding
时适用,子
Binding
中虽然也可以设置
StringFormat
,但是会被忽略。

PriorityBinding

相比于前两种绑定,
PriorityBinding
使用的频率没那么高,它的主要作用是按照一定优先级顺序设置绑定列表, 如果最高优先级绑定在处理时成功返回值,则无需处理列表中的其他绑定。 如果计算优先级最高的绑定需要很长时间,那么将会使用成功返回值的次高优先级,直到优先级较高的绑定成功返回值。
PriorityBinding
和其包含的绑定列表中的子
Binding
也都可以设置
StringFormat
属性。例如:

<TextBlock
    Width="100"
    HorizontalAlignment="Center"
    Background="Honeydew">
    <TextBlock.Text>
        <PriorityBinding FallbackValue="defaultvalue" StringFormat="haha:{0}">
            <Binding IsAsync="True" Path="SlowestDP" StringFormat="hi:{0}"/>
            <Binding IsAsync="True" Path="SlowerDP" />
            <Binding Path="FastDP" />
        </PriorityBinding>
    </TextBlock.Text>
</TextBlock>


MultiBinding
不同的是,
PriorityBinding
的子
Binding
中的
StringFormat
是会生效的,其规则是优先使用子
Binding
设置的格式,其次才使用
PriorityBinding
设置的格式。

Content属性格式化失效的原因

Button

Content
属性可以用字符串赋值并显示在按钮上,但是使用
StringFormat
格式化并不会生效。原本我以为是涉及到类型转换器,在类型转换过程中处理掉了,但这只是猜测,通过源码发现并不是这样的。在
BindingExpressionBase
中有这样一段代码:

internal virtual bool AttachOverride(DependencyObject target, DependencyProperty dp)
{
	_targetElement = new WeakReference(target);
	_targetProperty = dp;
	DataBindEngine currentDataBindEngine = DataBindEngine.CurrentDataBindEngine;
	if (currentDataBindEngine == null || currentDataBindEngine.IsShutDown)
	{
		return false;
	}
	_engine = currentDataBindEngine;
	DetermineEffectiveStringFormat();
	DetermineEffectiveTargetNullValue();
	DetermineEffectiveUpdateBehavior();
	DetermineEffectiveValidatesOnNotifyDataErrors();
	if (dp == TextBox.TextProperty && IsReflective && !IsInBindingExpressionCollection && target is TextBoxBase textBoxBase)
	{
		textBoxBase.PreviewTextInput += OnPreviewTextInput;
	}
	if (TraceData.IsExtendedTraceEnabled(this, TraceDataLevel.Attach))
	{
		TraceData.TraceAndNotifyWithNoParameters(TraceEventType.Warning, TraceData.AttachExpression(TraceData.Identify(this), target.GetType().FullName, dp.Name, AvTrace.GetHashCodeHelper(target)), this);
	}
	return true;
}

其中第11行调用了一个名为
DetermineEffectiveStringFormat
的方法,顾名思义就是检测有效的
StringFormat
。接下来看看里边的逻辑:

internal void DetermineEffectiveStringFormat()
{
	Type type = TargetProperty.PropertyType;
	if (type != typeof(string))
	{
		return;
	}
	string stringFormat = ParentBindingBase.StringFormat;
	for (BindingExpressionBase parentBindingExpressionBase = ParentBindingExpressionBase; parentBindingExpressionBase != null; parentBindingExpressionBase = parentBindingExpressionBase.ParentBindingExpressionBase)
	{
		if (parentBindingExpressionBase is MultiBindingExpression)
		{
			type = typeof(object);
			break;
		}
		if (stringFormat == null && parentBindingExpressionBase is PriorityBindingExpression)
		{
			stringFormat = parentBindingExpressionBase.ParentBindingBase.StringFormat;
		}
	}
	if (type == typeof(string) && !string.IsNullOrEmpty(stringFormat))
	{
		SetValue(Feature.EffectiveStringFormat, Helper.GetEffectiveStringFormat(stringFormat), null);
	}
}

这段代码的作用就是检测有效的
StringFormat
,并通过
SetValue
方法保存起来,从第4~7行代码可以看到,一开始就会检测目标属性的类型是不是
String
类型,不是的话直接返回,绑定表达式中的
StringFormat
也就不会保存了。在后续的
BindingExpression
类计算绑定表达式值时获取到
StringFormat

null
,也就不会进行格式化了。
image

Button

Content
属性虽然可以用字符串赋值,但它其实的
Object
类型。因此,在检测有效的
StringFormat
表达式时直接过滤了。
ToolTip
也同样是
Object
类型。
image

解决方法

对于
Content
这种
Object
类型的属性绑定字符串并且需要格式化时,可以采用以下三种方式解决:

  1. 最通用的方法就是自定义
    ValueConverter
    ,在
    ValueConverter
    中对字符串进行格式化;
  2. 绑定到其他可进行
    StringFormat
    的属性上,比如
    TextBlock

    Text
    属性进行格式化,
    ToolTip
    绑定到
    Text
    上;
  3. 既然是
    Object
    类型,那也可把
    TextBlock
    作为
    Content
    的值。
<Button Width="120" Height="30">
    <Button.Content>
        <TextBlock Text="{Binding TestString,ElementName=self,StringFormat=test:{0}}"/>
    </Button.Content>
</Button>

小结

数据绑定时出现StringFormat失效的主要分为两种情况。一是没有遵循绑定时StringFormat使用的约束,二是绑定的目标属性不是
String
类型。

每个人的职业生涯都是一段充满转折和挑战的旅程,当然每一次职业转型都是一次重新定义自己的机会,从2015年开始,当时我刚踏入IT行业,成为一名Java开发者,后来随着时间的推移,我的职业方向逐渐转向了前端开发者,埋头于代码的世界。最终在2018年找到了属于自己的职业定位——产品经理。一路走来,我不断扩展自己的技能边界,从代码的深度探索,到产品的全面把控,这段经历不仅是我职业发展的缩影,也是我对技术与战略结合的深刻体会。

2015年 - Java开发的初体验

2015年,我进入了IT行业,开始了我的Java开发之旅。那时,我的世界充满了代码、算法和数据库。我热衷于编写后台逻辑,解决复杂的技术难题。第一次用Java成功搭建一个系统时,那种成就感至今难忘。这一年让我扎实掌握了Java的核心技能,培养了严谨的逻辑思维和解决问题的能力。然而,随着项目的深入,我开始对仅仅专注于后台开发感到一些局限。

2016年 - 前端开发的跃升

2016年,我决定拓展自己的技术视野,转向前端开发。我想更贴近用户,了解他们如何与产品互动。转型后的日子充满了挑战,从学习HTML、CSS到掌握JavaScript,我一步步走入了前端的世界。最令我兴奋的是,看到自己的代码直接呈现在用户面前,创造出视觉上赏心悦目的网页和应用。前端开发不仅让我更了解用户体验的重要性,还让我逐渐意识到,一个优秀的产品不仅仅依赖于技术,更在于用户需求的精准把握。

2018年 - 从技术到产品的转型

然而,在与产品经理合作的过程中,我逐渐意识到,自己对整个产品的愿景、设计逻辑以及用户需求有着强烈的兴趣。我不再满足于只完成技术任务,而是开始思考:为什么要这样设计?这个功能对用户来说真的有用吗?我渴望跳出代码的框框,去探索更广阔的产品世界。

于是在2018年,我做出了一个重大决定:从技术开发转型为产品经理。这并不是一条平坦的道路,我需要快速掌握市场分析、需求定义、产品设计、项目管理等全新的技能,起初,这些新领域让我感到陌生,但也激发了我无尽的好奇心。我开始参与制定产品策略,协调跨职能团队,并主导项目的从零到一的全过程。在这个过程中,我深刻体会到,产品经理不仅要有技术背景,还需要具备战略眼光和用户思维。

结语

从Java开发到前端开发,再到产品管理,我的职业经历是一段不断探索、不断挑战自我的旅程。这些年的转型让我深刻认识到,技术只是职业发展的起点,而对用户、市场和战略的把握,才是成就一个产品经理的关键。在未来的职业道路上,我将继续融合技术与管理的优势,推动更多创新产品的诞生,实现更大的职业突破。

接下来,我会分享从0到1设计获客系统和支付系统的实战干货,每一步都有亲身经验和独家技巧。为了让你更直观地理解,我将重点探讨以下内容:

获客系统:

1.短视频获客系统的背景与重要性

为什么短视频在当今市场如此关键?我会深入解析其背后的逻辑和发展趋势。

2.市场现状与竞品分析

了解市场动态和竞争对手是设计获客系统的基础。我会分享如何高效进行市场调研和竞品分析。

3.需求分析

确定目标用户的需求是系统设计的核心,我将展示如何精准把握用户需求并转化为系统需求。

4.
商业模式设计

成功的获客系统不仅仅是技术的体现,更需要与商业模式相辅相成。我会剖析如何设计出既能吸引用户又能带来收益的商业模式。

5.
功能规划

系统功能的规划直接影响用户体验和系统的可扩展性,我会分享功能规划的关键策略。

6.
产品设计

最后,我会讲解如何将以上内容融入实际的产品设计中,从概念到实现,全流程详解。

如果你也渴望快速提升自己的产品设计能力,欢迎加入我们的学习群或者我个人微信,在这里我们可以一起交流、共同成长!机会难得,千万别错过哦!