Quantcast
Channel: Andy Blog
Viewing all 113 articles
Browse latest View live

AI Tutorial 2

$
0
0

Introduction

用AI制作一个女性的一生 用deepseek/chatgpt 生成指令 用剪映(即梦海外版)生成图片 用可灵生成视频 用剪映电脑版剪辑视频 用YouTube发布

注册可灵AI国际版(klingai)

klingai official website使用可灵ai国际版注册 > sign in with email

现在需要若干邮箱 > https://maildrop.cc/ > 有2种方法获取免费邮箱

继续可灵AI注册 > 邮箱收到6位验证码 > 输入注册框 > next > 注册成功登录> 每账户每月166个免费积分,如果不够用就注册新账号

# 注册剪映AI国际版(capcut) capcut

使用剪映ai国际版注册 > sigup in with email

用剪映(capcut)生成不同年龄的中国女性图片

可以用 deepseek/chatgpt 写一段图片的详细描述(指令脚本),注意要用英文,跟剪映国际版的语言匹配

中文指令参考:

一个18岁漂亮的女生 /英俊的男生,真实的照片质感
正面半身照一个24岁漂亮的女人/英俊的男人,真实的照片质感,正面半身照
一个15岁漂亮/英俊的小孩,真实的照片质感,正面半身照
一个10岁可爱的小孩,真实的照片质感,正面半身照
一个5岁可爱的小孩,真实的照片质感,正面半身照

回到capcut > 粘贴脚本 > 生成

如果对纯文字生成的图片不满意,可以找一张参考图,让ai根据参考图精确生成图片 譬如 google > image > 中国漂亮女孩 白衣 全身照 高清图 > 选一张图

然后不断更换指令脚本,分别生成5岁,10岁,18岁,30岁的照片,挑出满意的

用 可灵AI 分段制作视频

image > to video > 选好首帧,尾帧 > 点击生成,每次消耗35个点 > 生成约3分钟

用剪映电脑版剪辑成完整视频

英文版:https://www.capcut.com/ > 下载capcut desktop > 中文版:https://www.capcut.cn/

用YouTube发布视频

FAQ

如何切换可灵账户

References

【完整教程】爆火AI小女孩从1岁到80岁制作教程拆解,纯原创制作,日入多张 | 老高项目网]()


AI Tutorial 3

$
0
0

Introduction

用AI制作老奶奶鸡汤

用deepseek生成文案

指令:

帮我写出6句可以让人缓解焦虑的鸡汤

根据文字制作图片

接着,使用上面的文案,一行一个作为配字,让AI画出治愈系老奶奶图片,指令参考下面的格式:

主体:中国老奶奶,体型偏胖,和蔼可亲,微笑,白发卷曲,戴圆眼镜,红衣服。

动作:双手持标语,内容“xxxxxxxxx”。

场景:室内门口,门半开,屋内有画框、装饰,门外有光线。

风格:彩色简笔画,卡通风格,治愈素插画,明亮色彩,细腻线条,温馨氛围,生动童趣,手绘风笔触

经测试,即梦海外版(capcut)对中文处理不好,无法理解生成手持标语的动作,生成的图片没有标语。如果用中文版,要选模型2.1才有标语

下面使用微软bing生成 https://www.bing.com/images/create

用剪映电脑版(capcut)根据图片剪辑视频

FAQ

References

一分钟教你学会,使用AI创作治愈系老奶奶鸡汤视频# 发作品可以赚钱的短视频平台(短视频挣钱的平台有哪些)

AI Tutorial 4

$
0
0

Introduction

用AI制作漫画小说 deepseek可以写文字,但不能生成图片 而chatgpt既可以写,又能生成图片,所以这里用chatgpt

生成小说大纲

指令如下:

现在你是一位网络畅销爽文作家,帮我写一篇短篇小说,一下是番茄小说网女频榜单畅销的书单:
唐总,离婚后请自重
李总,夫人不想复婚还偷偷生崽了
司总,你的小作精又受伤了
藏起孕肚离婚,顾总追妻火葬场!
退婚后,我竟查出肚子里有四胞胎

先帮我生成10个有吸引力的言情小说的书名。

挑一个书名,输入指令:

请帮我生成第9个书单的目录

下面写第一章:

请帮我生成第一章的内容,字数在1000字以内。要求情节紧凑,有恩怨情仇,男女主角之间有强烈的的化学反应。
下面是写作风格例子:
姜彤刚办理了离婚,后脚就发现她怀孕了.【帝景集团CEO一厉璟辰,今日份已经回归南帝市.】
两年过去,看到这条新闻的姜彤,赶紧藏好了和前夫长得如出一辙的小包子.藏不住了,只能带着
儿子跑路.那个杀伐决断的男人气势汹汹堵在门口,直接抱起她往床上丢."想带我的崽跑去哪?
嗯? "我错了.…唔."-小剧场:很久之后,姜彤去南山祈福,才看到厉璟辰在离婚那年挂在月老树的红条

制作男女主角形象

输入指令

小说男女主角是谁?

输入指令,让AIai生成具体的形象描述

让我们为这部小说的男女主角塑造更具体的形象:

输入指令生成图片:

我正在写一部网络畅销爽文,请帮我生成女主角形象的图片,要求日式漫画风格,比例16:9,背景明亮一些,人物可爱一些:

输入指令

这个图像的gen-id是什么

生成女主角

注:gen-id后面用来做reference 女主角gen-id: eca53c7f-ee60-4602-be5a-f0b6e07595ec 男主角gen-id: a033378c-71e1-4529-988a-0c635a70fe07

制作第一章的分镜图片集

输入指令

请把第一章生成对应的12个分镜图文字描述,每个分镜图要包括背景,人物服饰,情绪对话以及文字对话

下面制作分镜图#1:

日式动漫风格,比例16:9,gen-id:
描述xxxxxxxx

然后逐个生成分镜图2-12,不断训练纠正AI直到图片满意

用剪映电脑版根据图片制作第一章的视频

现在小说已经写好,系列分镜图已经做好,下面可以开始做视频 这里写的是中文小说,所以用剪映中文版,注意国际版的capcut不能解说中文小说。

剪映(capcut) > 图文成片(script to video) > 编辑文案(edit script) > 拷贝第一章内容 > 选解说(有时加载慢)> 生成视频

此时文字,解说会自动生成。下面导入刚才的分镜图,导入音乐,同步调整解说和分镜图。最后可以对分镜图逐个加一些转场特效,以免观众对静态图审美疲劳

FAQ

References

一分钟教你学会,使用AI创作治愈系老奶奶鸡汤视频

AI Tutorial 5

$
0
0

Introduction

用AI制作治愈vlog动画,是制作漫画小说的后续

用chatgpt生成文案

参考对标账号,可以存一些样图 从评论里找灵感,改进文案

制作女主角形象

用midjourney生成女主角

输入指令

In the style of Studio Ghibli, close-up, a young woman in her 20s to 30s, with long black hair either naturally loose or caually tied up. She has a gental and smoothing expression with a warm smile, and healthy complexion.

生成分镜镜头

人物正面,指令上加–cref, 控制人物一致性

人物侧面或背面,要用准确的提示词文字,锁定人物特征

图片转视频,让图片动起来

luma

用剪映剪辑成片

添加背景音乐 添加动态特效 添加环境音效,譬如开门声,炒菜声,洗澡声,呼噜声。。。

FAQ

References

# 用AI做治愈動畫vlog視頻

AI Tutorial 6

$
0
0

Introduction

用AI制作奇幻场景,实景照片结合AI

准备图片素材

前期可以从网上找; 后期如果形成固定风格,素材不好找,可以用midjourney, mj3 生成

剪映加下雨,下雪特效

剪映 > 添加素材 > 搜索 下雪 绿幕 >

图片转视频,让图片动起来

luma

用剪映剪辑成片

添加背景音乐 添加动态特效 添加环境音效,譬如开门声,炒菜声,洗澡声,呼噜声。。。

FAQ

References

# AI製作治癒風景視頻,7天漲粉10萬|圖片轉動畫/落地雲/室內外下雨下雪/動態壁紙

PowerShell Tutorial

$
0
0

Introduction

Windows PowerShell is a command-line shell and scripting language designed especially for system administration. It’s analogue in Linux is called as Bash Scripting. Built on the .NET Framework, Windows PowerShell helps IT professionals to control and automate the administration of the Windows operating system and applications that run on Windows Server environment.

v5 vs v7

v5 based on .net 4.5 v6 based on .net core 2.0. already expired v7 based on .net core 8.0

Install

Install v7

IDE

  1. PowerShell ISE, build-in at windows system, support until v7
  2. Install visual studio code extension, recommended

    Cmdlets

    cmdlet (pronounced “command-let”) is a compiled command. A cmdlet can be developed in .NET or .NET Core and invoked as a command within PowerShell. Thousands of cmdlets are available in your PowerShell installation. You can create and invoke them programmatically through Windows PowerShell APIs.

Cmdlets are named according to a verb-noun naming standard. This pattern can help you to understand what they do and how to search for them. It also helps cmdlet developers create consistent names. You can see the list of approved verbs by using the Get-Verb cmdlet. Verbs are organized according to activity type and function.

$PSVersionTable

$PSVersionTable.PSVersion

Get-Variable

`Get-Verb

Get-Command

Get-Help

Get-Command -Noun alias*

Get-Command -Verb Get -Noun alias*

Get-Command -Noun File*

Get-Command -Verb Get -Noun File*

Location Get-Service “vm*” | Get-Member`

Scripting

variables

Variable name should start with $ and can contain alphanumeric characters and underscore $location = Get-Location

special variables $$$?$_https://www.tutorialspoint.com/powershell/powershell_special_variables.htm

Error & Exceptions

Write-Output是标准输出

Write-Output "FFmpeg finished successfully"
Write-Output 用于向调用者返回正常结果。
它输出的是你期望正常处理的数据。
在 PowerShell SDK 中,它的内容会进入 PowerShell.Invoke() 的返回结果中。 C# powershell sdk中接收 ``` var results = ps.Invoke(); foreach (var item in results)
Console.WriteLine(item); // 输出 Write-Output 的结果 ``` ## `Write-Error` 是错误流 ``` Write-Error "FFmpeg failed to process input" ```
Write-Error 用于报告发生了错误或异常。
它不会中断脚本(除非用 -ErrorAction Stop),但会被 PowerShell SDK 捕获为错误流。 在 C# 中,它会出现在  ``` ps.Streams.Error ```  C# powershell sdk中接收方式: ``` if (ps.Streams.Error.Count > 0) {
foreach (var err in ps.Streams.Error)
    Console.WriteLine("错误: " + err); // 输出 Write-Error 的内容 } ``` ## 标准写法 ProcessStartInfo ``` var psi = new ProcessStartInfo
    {
        FileName = "powershell.exe",
        Arguments = $"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\" {arguments}",
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        CreateNoWindow = true,
    }; using (var process = Process.Start(psi)) {
string stdOutput = process.StandardOutput.ReadToEnd();
string stdError = process.StandardError.ReadToEnd();

process.WaitForExit();

// 输出日志
Console.WriteLine("=== FFmpeg 标准输出 ===");
Console.WriteLine(stdOutput);

if (!string.IsNullOrWhiteSpace(stdError))
{
	Console.WriteLine("=== FFmpeg 错误输出 ===");
	Console.WriteLine(stdError);

	// 你也可以在这里抛出异常
	throw new Exception("FFmpeg 执行失败: \n" + stdError);
}

if (process.ExitCode != 0)
{
	throw new Exception($"FFmpeg 执行失败,退出代码: {process.ExitCode}");
} } ``` SDK ``` using (PowerShell ps = PowerShell.Create()) {
ps.AddCommand("Set-ExecutionPolicy").AddParameter("Scope", "Process").AddParameter("ExecutionPolicy", "Bypass").Invoke();
ps.Commands.Clear();

ps.AddScript($"& '{scriptPath}'");

var results = ps.Invoke();

foreach (var outputItem in results)
{
	Console.WriteLine(outputItem.ToString());
}

if (ps.Streams.Error.Count > 0 || ps.HadErrors)
{
	foreach (var error in ps.Streams.Error)
	{
		Console.WriteLine("PowerShell Error: " + error.ToString());
	}

	throw new Exception("FFmpeg 脚本执行失败。");
} } ``` ## Exception PowerShell 支持抛出异常(`throw`),而且 **C# 使用 PowerShell SDK 调用时是可以捕获这些异常的**,不过捕获的位置和方式与普通 .NET 异常略有不同。 ``` throw "Something went wrong" or throw [System.Exception]::new("Explicit exception from PowerShell") ``` PowerShell 的 `throw` 在 **PowerShell SDK 中不会直接变成 C# 的 try/catch 异常**,而是会被捕获进: ``` ps.Streams.Error ``` 强制抛出错误 ``` if ($process.ExitCode -ne 0) {
$errorMessage = Get-Content "stderr.txt" -Raw
throw "ffmpeg failed with exit code $($process.ExitCode): `n$errorMessage" } ``` ## ffmpeg 注意powershell**不会**捕捉 `ffmpeg` 之类 CLI 工具输出的错误信息。如果powershell调用ffmpeg,**PowerShell SDK** 中不会自动将 `ffmpeg.exe` 的错误写入 `ps.Streams.Error`,因为它是子进程行为(非 PowerShell 命令)。
FFmpeg 是外部可执行程序(非 PowerShell cmdlet),它的错误信息**默认写入标准错误流(stderr)**,而非 PowerShell 的 `$Error` 或 `ps.Streams.Error`。 `ffmpeg`(和大多数 CLI 工具)**总是把普通日志写到 stderr(标准错误流)**,比如: ``` `ffmpeg version ...    Input #0, ...   Stream mapping:   ... ```

即使执行成功,这些“正常日志”也会进 stderr,而 PowerShell 会把 stderr 内容封装进 ErrorRecord放到 ps.Streams.Error里。所以下面方式是无法捕获到ffmpeg异常

ps.Streams.Error
or
s.HadErrors == true

if ($process.ExitCode -ne 0) { $errorMessage = Get-Content “stderr.txt” -Raw throw “ffmpeg failed with exit code $($process.ExitCode): `n$errorMessage” }

Best practice

Powershell完全忽略 ps.HadErrors,只用 $LASTEXITCODE让脚本决定是否真的失败。

  • 不要依赖 FFmpeg 的 stderr 内容
  • 使用 $LASTEXITCODE判断成功
  • 使用Write-Error将错误写入 PowerShell.Streams.Error(C# 能读取)
  • 使用 throw抛错:让 PowerShell 脚本中止执行,也便于调试
  • $_$_.Exception.Message:确保抛出完整错误信息(FFmpeg 或逻辑错误

c#端

  • 调用 ps.Invoke()
  • 完全信任 $LASTEXITCODE + 自定义错误
  • ps.Invoke()本身不会返回 $LASTEXITCODE,因为 $LASTEXITCODE是 PowerShell 的内部变量,System.Management.Automation.PowerShell API 并不会自动将它暴露给 C#。 我们需要在 PowerShell 脚本中将 $LASTEXITCODE显式输出,C# 才能获取并检查它。
  • 不信任ps.HadErrors
  • 使用ps.Streams.Error获取错误信息(当然对于ffmpeg这个也输出正常信息)

基于这些分析。

powershell:

try{
...
    # 检查输出是否成功完成
    if ($LASTEXITCODE -ne 0) {
        $errorMsg = "FFmpeg failed with exit code $LASTEXITCODE"        Write-Error $errorMsg
        throw $errorMsg
    }
    else {
        Write-Output "FFmpeg succeed: $OutputPath"    }
    # 正常结尾在脚本尾部加一个标识格式输出:
    Write-Output "##SCRIPT_EXIT_CODE:$LASTEXITCODE"    exit 0
}
catch {
    Write-Error "Script error: $($_.Exception.Message)"    # 加标识
    Write-Output "\n##SCRIPT_EXIT_CODE:$LASTEXITCODE"    throw $_
}

C# SDK 如果 FFmpeg 报错并在 PowerShell 脚本中写出 exit 1,在 C# 中通过解析 ##SCRIPT_EXIT_CODE:1获取真正的失败。

            // 捕捉错误
            // 提取 PowerShell 输出中的 exit code(如果你在脚本中输出了它)
            int? exitCode = null;
            foreach (var item in results)
            {
                string? line = item?.ToString();
                if (line != null && line.StartsWith("##SCRIPT_EXIT_CODE:"))
                {
                    var codePart = line.Substring("##SCRIPT_EXIT_CODE:".Length);
                    if (int.TryParse(codePart.Trim(), out var parsedCode))
                    {
                        exitCode = parsedCode;
                    }
                }
            }

            // 检查 exit code 是否非0
            if (exitCode.HasValue && exitCode.Value != 0)
            {
                //throw new Exception($"Script exited with code {exitCode.Value}.");
                // 处理 PowerShell Streams 错误
                if (ps.HadErrors)
                {
                    var realErrors = ps.Streams.Error
                        .Where(e =>
                            !string.IsNullOrWhiteSpace(e?.ToString()) &&
                            !e.ToString().Contains("ffmpeg version", StringComparison.OrdinalIgnoreCase) &&
                            !e.ToString().Contains("Input #0", StringComparison.OrdinalIgnoreCase)
                        )
                        .ToList();

                    if (realErrors.Count > 0)
                    {
                        var message = string.Join(Environment.NewLine, realErrors.Select(e => e.ToString()));
                        throw new Exception("PowerShell script error:\n" + message);
                    }
                }

            }
特性Write-OutputWrite-Error
用途返回正常数据 / 结果报告错误 / 异常
是否终止脚本否(除非指定 -ErrorAction Stop
SDK 结果位置ps.Invoke()的返回值ps.Streams.Error
输出到控制台打印到标准输出(stdout)打印到错误输出(stderr)
建议场景正常结果,如文件路径、状态、文本报错信息,如找不到文件、无权限等

FAQ

How to call powershell script from c# code?

https://learn.microsoft.com/en-us/powershell/scripting/developer/hosting/adding-and-invoking-commands?view=powershell-7.4

https://learn.microsoft.com/en-us/answers/questions/1334905/c-process-how-to-execute-powershell-command-with-a

https://stackoverflow.com/questions/26184932/get-powershell-errors-from-c-sharp

https://stackoverflow.com/questions/74016517/declare-a-variable-to-ffmpeg https://stackoverflow.com/questions/8213865/ffmpeg-drawtext-over-multiple-lines

How to escape special character?

Use `` or ` for example:

`$ for $
`# for #
`" for "
`( for (
\`: for :
\\% for %
 ``' for '

# How to define functions in Powershell? way1

 function splitStringBySizeByWord([string]$string, [int]$size){
 ....
 }

way2

 function splitStringBySizeByWord{

    Param([string]$string, [int]$size)
    ....
    }

how to call:

# import
. "C:\Workspace\...\string.ps1"

....
$multiLineString = splitStringBySizeByWord -string $string -size $size;

token ‘&&’ is not a valid statement separator in this version?

这种写法:

ffmpeg -i input.mp4 output.mp4 && echo "done"

主要问题是:Windows PowerShell 5.x 没有内置 &&操作符(直到 PowerShell 7+ 才支持 &&||) 改为使用 if ($?);

ffmpeg -i input.mp4 output.mp4; Write-Host "转换完成"

ffmpeg -i input.mp4 output.mp4
if ($?) {
    Write-Host "Done"
} else {
    Write-Host "Error occurred"
}

ffmpeg -i input.mp4 output.mp4; Write-Host "转换完成"

Introduction to PowerShellPowershell TutorialPoweShell 101PowerShell DocsMigrating from Windows PowerShell 5.1 to PowerShell 7Differences between Windows PowerShell 5.1 and PowerShell 7.xPowerShell Support Lifecycle

PowerShell Tutorial

$
0
0

Install v7 first

https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows

How to set v7 as default in windows

让整个 Windows 系统默认使用 PowerShell 7(pwsh)代替 PowerShell 5(powershell.exe),可以通过以下方式实现。这里分为 用户级别系统级别的设置:

在 Windows Terminal 中设置默认使用 PowerShell 7

安装完 PowerShell 7 后,Windows Terminal 会自动识别它。

设置方法:

  1. 打开 Windows Terminal,选Powershell 7,不要选windows poweshell

  2. 点击右上角下拉菜单 → Settings

  3. 在 “Startup” → Default Profile选择 PowerShell
  4. 保存即可

检查当前powershell版本

$PSVersionTable.PSVersion

powershell命令设置别名(仅限你自己)

修改快捷方式、别名 powershellpwsh设置方式:

如果你用的是 WSL 或其他 bash/zsh:

alias powershell=pwsh

在PowerShell 里设置别名(仅当前会话)

Set-Alias powershell pwsh但这只对当前会话有效,关闭窗口就失效。

永久设置别名(修改 PowerShell 配置文件),需要在 PowerShell 配置文件 $PROFILE中加入:

具体方法: 打开powershell

notepad $PROFILE

在打开的文件中加入:

Set-Alias powershell pwsh

保存后关闭。下次启动 PowerShell 就自动生效。 这样你在终端中输入 powershell就会自动变成运行 PowerShell 7 的 pwsh

💡 如果 $PROFILE 文件不存在,上面的命令会自动创建。

修改 PATH 环境变量顺序

把 PowerShell 7 路径排到前面

  1. 打开:控制面板 → 系统 → 高级系统设置 → 环境变量 (environement)

  2. 找到系统的 Path变量

  3. 将以下路径移动到前面:

    C:\Program Files\PowerShell\7\

  4. 确保它在如下路径前面:

    C:\Windows\System32\WindowsPowerShell\v1.0\

  5. 重启命令提示符、VSCode、终端等生效

📌 注意:这并不能改变 powershell.exe,但会让你输入 pwsh时优先使用 PowerShell 7。

设置 VS Code 中默认使用 PowerShell 7

重启visual studio code 打开命令面板:Ctrl + Shift + P输入并选择:Terminal: Select Default Profile选择带有 PowerShell 7 的选项(通常路径是 C:\Program Files\PowerShell\7\pwsh.exe安装扩展:PowerShell Extension

  1. 打开命令面板(Ctrl + Shift + P)输入:

    PowerShell: Select Interpreter

  2. 选择路径为:

    C:\Program Files\PowerShell\7\pwsh.exe可以在vs code 的terminal 里检查powershell版本进一步确认

    Visual studio 2022 设置v7为默认

    将项目从.net 8.0 升级到9.0

    安装nuget library Microsoft.PowerShell.SDK这样就能使用powershell的sdk了 还有一个 System.Management.Automation,非官方,不推荐

    设置terminal

       - 打开 Visual Studio 2022    
       - 点击菜单栏 → `工具 (Tools)` → `选项 (Options)`    
       - 在弹出的窗口中,导航到 终端 (Terminal)`
       - 点击 “添加” 添加一个新的配置:
    
  • 名称 (Name): PowerShell 7

  • 命令行路径 (Command line path): C:\Program Files\PowerShell\7\pwsh.exe
  • 工作目录保持默认或自定义
  • 确认保存并关闭

以后在 VS 中打开“终端窗口”时(视图 → 终端Ctrl + ~),将使用 PowerShell 7

修改代码

如你代码中写的是:

Process.Start("powershell.exe", ...);

想改为 PowerShell 7,则只需要将文件名替换为完整路径:

Process.Start(@"C:\Program Files\PowerShell\7\pwsh.exe", ...);

如果你不想写死路径,也可以读取环境变量或在 PATH中优先设置 PowerShell 7 路径(见之前回答)

Visual studio c#项目调用powershell script

v7 使用powershell sdk

using System; using System.Management.Automation;

class Program { static void Main() { bool isDebug = true;

    using (PowerShell ps = PowerShell.Create())
    {
        // 加载脚本
        ps.AddCommand(@"C:\Path\To\test.ps1");
        ps.AddParameter("isDebug", isDebug);

        // 执行脚本
        var results = ps.Invoke();

        // 输出返回结果
        foreach (var result in results)
        {
            Console.WriteLine(result.ToString());
        }
    }
} } ## v5, v7 使用Process.Start bool isDebug = true; string boolArg = isDebug ? "$true" : "$false";

var psi = new ProcessStartInfo() { FileName = “powershell”, Arguments = $@”-ExecutionPolicy Bypass -File ““C:\Path\To\test.ps1”” -isDebug {boolArg}”, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true };

using (var process = Process.Start(psi)) { string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); Console.WriteLine(output); }

powershelll sdk vs ProcessStartInfo

PowerShell SDK API调用脚本,这是比 ProcessStartInfo更强大、结构化的方式,优点非常明显。 vs project -> powershell.create() > 启动 powershell sdk > .net core 9.0 > powershell v7,开发方便,我选这种

vs project > processtartinfo > .启动 powershell.exe> net core 9.0 or .net framework 4.x > powershell 5.x/7.x,兼容性好但开发略麻烦

PowerShell.Create()(你的代码):

  • 你已经引用了 Microsoft.PowerShell.SDK,在 C# 项目中内嵌调用 PowerShell。

  • 参数多,需传复杂对象或结构化数据。

  • 希望精确控制命令执行、结果解析、错误处理。

✅ 用 ProcessStartInfo

  • 快速调用 .ps1脚本,无需添加 SDK。

  • 脚本是独立运行(非嵌入),适合部署时与系统 PowerShell 配合使用。

  • 不关心结构化返回,只要拿到输出结果。

    FAQ

    vs 项目升级到了.net 9.0,提示错误:Unhandled exception. System.Management.Automation.Runspaces.PSSnapInException: Cannot load PowerShell snap-in Microsoft.PowerShell.Diagnostics because of the following error: Could not find file ‘…\bin\Release\net9.0\runtimes\win\lib\net9.0\Microsoft.PowerShell.Commands.Diagnostics.dll

    安装 Microsoft.PowerShell.SDK 确认所有关联项目都升级到了9.0

    vs项目 运行出错,Unhandled exception. System.Management.Automation.PSSecurityException: File …\test-boolean.ps1 cannot be loaded because running scripts is disabled on this system

    说明你当前系统的 PowerShell 执行策略 (ExecutionPolicy)禁止运行 .ps1脚本。这是 Windows 系统的一个默认安全设置。

    推荐方案:在调用时指定临时“Bypass”策略

如果你是通过 C# 代码调用脚本,最安全也最简单的做法是添加这一行参数:

Arguments = $"-ExecutionPolicy Bypass -File \"{scriptPath}\""

或者,你也可以改当前用户的策略(不会影响其他人):

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

Sqlite Tutorial

$
0
0

Introduction

SQLite是一个轻量级、嵌入式的开源数据库引擎。

  • 零配置:无需安装服务器或守护进程

  • 单文件数据库:所有数据保存在一个 .sqlite.db文件中

  • 跨平台支持:可运行于 Windows、Linux、macOS、Android、iOS 等

  • 内置于多种应用和语言中:如 Python、Android、Firefox、Chrome、Git 等

    📦使用场景

  • 移动应用(如 Android、iOS)

  • 桌面软件的数据存储(如记事本++ 插件)

  • 嵌入式系统(如 IoT、车载系统)

  • 配置文件存储或缓存层

  • 单用户应用程序

  • 单元测试中的临时数据库

⚠️ 不适用场景

SQLite 并非万能,不适合以下情况:

  • 高并发多用户写入

  • 大型企业级系统或分布式系统

  • 大量数据分析 / 多表 JOIN 的复杂查询

    Install

    download at sqlite download download full version : sqlite-tools-win-x64-3500100.zip 查看版本并启动 sqlite3 sqlite> .help

– 创建表 CREATE TABLE Users ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL, Age INTEGER );

– 插入数据 INSERT INTO Users (Name, Age) VALUES (‘Alice’, 30);

– 查询 SELECT * FROM Users WHERE Age > 25;

SQLite statements

SQLite ANALYZE Statement

ANALYZE; 
ANALYZE database_name; 
ANALYZE database_name.table_name;

GLOB Clause, 区分大小写

SELECT column1, column2....columnN 
FROM table_name 
WHERE column_name GLOB { PATTERN };

匹配模式使用通配符:
- `*`:匹配任意长度的任意字符    
- `?`:匹配单个任意字符    
- `[abc]`:匹配指定集合中任一字符    
- `[a-z]`:匹配字符范围

ALTER TABLE

ALTER TABLE table_name ADD COLUMN column_def...;
ALTER TABLE table_name RENAME TO new_table_name;

Transaction

BEGIN; 
or 
BEGIN EXCLUSIVE TRANSACTION;

COMMIT;

SAVEPOINT

SAVEPOINT savepoint_name;
RELEASE savepoint_name;

ROLLBACK

ROLLBACK; 
or 
ROLLBACK TO SAVEPOINT savepoint_name;

CREATE INDEX

CREATE INDEX index_name ON table_name ( column_name COLLATE NOCASE );
CREATE UNIQUE INDEX index_name ON table_name ( column1, column2,...columnN);
DROP INDEX database_name.index_name;

REINDEX

REINDEX collation_name; 
REINDEX database_name.index_name; 
REINDEX database_name.table_name;

CREATE TRIGGER

CREATE TRIGGER database_name.trigger_name 
BEFORE INSERT ON table_name FOR EACH ROW 
BEGIN 
stmt1; stmt2; .... 
END;

DROP INDEX database_name.trigger_name;

CREATE VIEW

CREATE VIEW database_name.view_name AS SELECT statement....;
DROP INDEX database_name.view_name;

CREATE VIRTUAL TABLE

CREATE VIRTUAL TABLE database_name.table_name USING weblog( access.log ); 
or 
CREATE VIRTUAL TABLE database_name.table_name USING fts3( );

EXISTS

SELECT column1, column2....columnN FROM table_name WHERE column_name EXISTS (SELECT * FROM table_name );

EXPLAIN Statement

EXPLAIN INSERT statement...; 
or
EXPLAIN QUERY PLAN SELECT statement...;

PRAGMA statement PRAGMA是 SQLite 提供的数据库内部设置命令,用于控制数据库引擎的各种底层行为,或获取其内部信息。它是一个特殊的 命令/语句,用于查询或设置数据库的运行参数、行为模式、性能选项、安全策略等。

PRAGMA pragma_name; 
For example: 
PRAGMA page_size; 
PRAGMA cache_size = 1024; 
PRAGMA table_info(table_name);

Programming

data type

sqlite采用类型亲和性系统,他的数据类型可以有很多种,creat table时都能使用,很灵活

| 声明类型示例(可以写任意名字)定义时可以随便使用 | SQLite 归类的类型亲和性 | 底层内部存储类型(Storage Class) | 说明 | | ————————– | ————— | —————————- | —————————— | | INTEGERINTBIGINT | INTEGER | INTEGER | 任意大小整数,最大 8 字节 | | REALFLOATDOUBLE | REAL | REAL | 浮点数,IEEE 8 字节 | | TEXTCHAR(n)VARCHAR | TEXT | TEXT | 字符串(UTF-8 / UTF-16), up to 1gb | | BLOBBYTEA | BLOB | BLOB | 二进制大对象, up to 1gb | | NUMERICDECIMAL | NUMERIC | INTEGER / REAL / TEXT / NULL | 精度优先,会尝试以整数或浮点存储 | | | | NULL | 空值 | ### boolean SQLite 没有专门的 BOOLEANDATETIME类型,但你可以通过 类型亲和性(Type Affinity)机制 + 约定方式来处理这两种类型。 用 INTEGER来表示布尔值.

|值|说明| |—|—| |0|FALSE| |1|TRUE|

is_active INTEGER NOT NULL DEFAULT 0

插入时用 0/1,或直接用 FALSE/TRUE(SQLite 会自动转换): INSERT INTO users (is_active) VALUES (TRUE);

datetime

SQLite 也没有 DATETIMEDATE类型,建议使用以下三种方式之一:

存储格式类型建议示例说明
文本格式TEXT'2025-06-18 21:05:00'ISO 8601 格式,推荐
整数时间戳INTEGER1729372800(UNIX 时间)秒(或毫秒)表示,可排序高效
浮点数REAL2459487.5(Julian)支持更高精度,但不常用

FAQ

Storage class和Affinity Type有什么区别,跟传统的data type什么关系

传统数据库的数据类型(Data Type)

大多数关系型数据库(如 SQL Server、MySQL、PostgreSQL)都有严格的数据类型系统,每个字段定义时绑定一个具体类型,比如 INT、VARCHAR(50)、DATETIME。 数据库会严格限制该字段只能存储该类型数据,否则报错或转换失败。

类型亲和性Type Affinity

SQLite 没有严格的类型系统,而是采用了“类型亲和性”机制: 类型亲和性:字段声明的类型名称被映射为五类亲和性之一,分别是:

| 类型亲和性(Affinity) | 说明 | 典型声明示例 | | ————— | —————– | ————————– | | INTEGER | 倾向存储整数 | INT, INTEGER, BIGINT | | REAL | 倾向存储浮点数 | REAL, FLOAT, DOUBLE | | TEXT | 倾向存储文本字符串 | TEXT, CHAR, VARCHAR | | BLOB | 无转换,原样存储二进制数据 | BLOB或无类型声明 | | NUMERIC | 数值类型,尝试存为整数或浮点或文本 | NUMERIC, DECIMAL | 当插入数据时,SQLite 会根据字段的类型亲和性尽量转换数据类型,但不会严格限制,比如你可以往 TEXT字段插入数字,反之亦然。

Storage Class(存储类别)

Storage Class是 SQLite 实际在磁盘和内存中存储数据时使用的物理类型,只有以下五种:

|Storage Class|说明| |—|—| |NULL|空值| |INTEGER|整数(最多8字节)| |REAL|浮点数(8字节)| |TEXT|字符串(UTF-8 或 UTF-16)| |BLOB|二进制数据,原样存储| 存储类别是数据实际存储格式,比如你给一个 TEXT 字段插入整数,SQLite 会根据规则决定是以 INTEGER 还是 TEXT 存储。

三者关系总结

|名称|作用|示例| |—|—|—| |传统数据类型|强类型定义字段,限制存储的数据类型|INT, VARCHAR(50), DATE| |SQLite 类型亲和性|字段声明时的类型指导,影响存储时的转换|声明 VARCHAR(20) => TEXT亲和性| |Storage Class|实际存储数据的物理类型|某行数据实际存为 INTEGER| 例如,

CREATE TABLE example (
  id INTEGER,       -- 类型亲和性 INTEGER
  name TEXT,        -- 类型亲和性 TEXT
  value NUMERIC     -- 类型亲和性 NUMERIC
);
INSERT INTO example VALUES ('123', 456, 78.9);
  • id字段:传入 '123',SQLite 尝试转换为 INTEGER,实际存储为 INTEGER 123。
  • name字段:传入数字 456,SQLite 转换为字符串 "456",存储为 TEXT。
  • value字段:传入浮点数 78.9,NUMERIC 亲和性尝试保留原类型,存储为 REAL。

    LIKE vs GLOB

    SQLite 中为什么同时存在 LIKEGLOB,它们到底有什么区别?

|特性|LIKE|GLOB| |—|—|—| |用途|SQL 标准的模糊匹配|Unix 风格的通配符匹配| |是否区分大小写|❌ 默认不区分(可配置)|✅ 始终区分大小写| |通配符语法|% = 任意长度,_ = 任意单个字符|* = 任意长度,? = 任意单个字符| |是否跨平台通用|✅ 是,通用 SQL 语法|❌ 否,仅限 SQLite| |转义机制|支持 ESCAPE字符|❌ 不支持转义| |语法扩展性|可自定义 Collation 和大小写规则|固定实现,不支持扩展| |性能差异|基本相同(简单匹配)|基本相同(都无法走索引)|

  1. 通配符语法不同
匹配意图LIKE语法GLOB语法
任意字符%*
单个任意字符_?
字符集合匹配❌ 不支持[abc]
字符范围匹配❌ 不支持[a-z]
  1. 大小写敏感性不同
-- 假设 name = 'Alice'

SELECT * FROM users WHERE name LIKE 'alice'; -- ✅ 能匹配
SELECT * FROM users WHERE name GLOB 'alice'; -- ❌ 不能匹配(区分大小写)

你可以通过设置 PRAGMA 让 LIKE 区分大小写:

PRAGMA case_sensitive_like = ON;

GLOB始终大小写敏感,不能改变。

  1. 转义字符支持
    SELECT * FROM files WHERE path LIKE '%\_%' ESCAPE '\';
    
    • LIKE支持 ESCAPE字符(可匹配字面上的 %_
    • GLOB不支持任何转义字符,*?总是通配
  2. SQL 标准支持
    • LIKE是 SQL 标准的一部分,几乎所有数据库都支持
    • GLOB是 SQLite 独有(模仿 Unix Shell 的 glob()),不具备跨数据库兼容性

Reference

https://www.tutorialspoint.com/sqlite/index.htm

https://www.sqlitetutorial.net/


WPF Tutorial 1

$
0
0

Introduction

WPF stands for Windows Presentation Foundation. It is a powerful framework for building Windows applications. 早起的GUI程序中,比如Windows Forms,程序的外观和行为没有完全分离,都用同一种语言开发,比如C#,往往需要同时修改UI和逻辑代码,维护成本高。 In WPF, UI elements are designed in XAML while behaviors can be implemented in procedural languages such C# and VB.Net. So it very easy to separate behavior from the designer code. With XAML, the programmers can work in parallel with the designers. The separation between a GUI and its behavior can allow us to easily change the look of a control by using styles and templates. WPF架构

Setup

Microsoft provides two important tools for WPF application development. Both the tools can create WPF projects, but the fact is that Visual Studio is used more by developers, while Blend is used more often by designers.

  • Visual Studio
  • Expression Blend XAML

    Conceptions

    Logical Tree vs Visual Tree

    Visual tree includes logic tree. 开发中常用-逻辑树,可以聚焦在写代码

    Dependency property

    依赖属性不是普通的c#内部属性字段,他可以注册到WPF的属性系统中统一管理。有很多新特性,比如双向绑定,样式,动画,资源等等,是一种超级属性。 依赖属性必须继承 DependencyObject class

    Routed Events

    本质上是一个事件event,能监听树。 有3种:

  • Direct Event - 等同与Windows form的event
  • Bubbling Event,从下往上传递,顶层是Window
  • Tunnel Event

    Controls

    WPF有100多种视觉控件。

    Data binding

    有多种数据绑定方法

    DataContext is used in View to bind data with ViewModel。最常用

     比如有一个view model ```csharp  public class Person { public string Name { get; set; } public int Age { get; set; } }

XAML 中这样绑定控件:
```xml
<!-- MainWindow.xaml -->
<StackPanel>
    <TextBlock Text="{Binding Name}" />
    <TextBlock Text="{Binding Age}" />
</StackPanel>

xaml代码中绑定数据源

publicpartialclassMainWindow:Window{publicMainWindow(){InitializeComponent();// 设置 DataContext(绑定源)this.DataContext=newPerson{Name="Alice",Age=30};}}

DataContext 层级继承行为

  • 如果你在 Window上设置了 DataContext,所有内部子控件默认继承它。
  • 你也可以在某个控件上局部替换 DataContext。 ```csharp
### 使用 `Binding.Source` 显式绑定(绕过 DataContext)
```xml
<Window.Resources>
    <local:Person x:Key="MyPerson" Name="Alice" Age="30"/>
</Window.Resources>

<TextBlock Text="{Binding Source={StaticResource MyPerson}, Path=Name}" />

好处:不依赖 DataContext,适合多个绑定源的情况
❗ 缺点:写法稍复杂,不够通用

使用 ElementName:控件之间绑定

<TextBoxx:Name="txtInput"Text="Hello"/><TextBlockText="{Binding ElementName=txtInput, Path=Text}"/>

📌 应用场景:两个控件之间同步内容,无需 ViewModel

使用 RelativeSource

<TextBlockText="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=Title}"/>

应用场景:从控件绑定到父窗口、父控件、模板宿主等
常见用法包括:

  • Self
  • FindAncestor
  • TemplatedParent

    绑定到静态类属性

    publicstaticclassGlobals{publicstaticstringAppVersion=>"1.0.0";}

    xaml ```xml xmlns:local=”clr-namespace:YourNamespace”

📌 WPF 支持 `x:Static`,WinUI 中可通过 `Binding Source={x:Bind}` 或使用静态资源类实现。
### 使用 Binding 对象绑定(纯 C#)
```csharp
Binding binding = new Binding("Name")
{
    Source = person,
    Mode = BindingMode.TwoWay
};

textBox.SetBinding(TextBox.TextProperty, binding);

📌 用于动态创建绑定、复杂逻辑、在后台绑定控件属性
常用于控件模板或控件库中

数据绑定方式对比总表

编号绑定方式示例语法是否依赖 DataContext优点缺点 / 限制常见用途
默认绑定(DataContext){Binding Name}✅ 是简洁、通用、支持 MVVM、自动继承上下文必须先设置 DataContext,否则绑定失败MVVM 绑定、页面绑定 ViewModel
显式设置 Source{Binding Path=Name, Source={StaticResource MyPerson}}❌ 否不依赖 DataContext,支持绑定多个对象写法冗长,无法继承多个绑定源场景、资源绑定
ElementName{Binding Path=Text, ElementName=txtInput}❌ 否控件之间联动,实时响应需设置好控件 Name,只限同一视觉树中使用输入框联动、用户控件交互
RelativeSource{Binding Path=Title, RelativeSource={RelativeSource AncestorType=Window}}❌ 否跨层级绑定、绑定模板外部对象写法复杂、调试困难控件绑定父级、模板控件绑定宿主
静态资源(x:Static){x:Static local:Globals.AppVersion}❌ 否常量绑定、静态属性绑定WPF 支持 x:Static,WinUI 需使用 x:Bind静态方法或类包装显示常量、系统信息、单例 ViewModel
C# 手动绑定(代码创建)textBox.SetBinding(TextBox.TextProperty, new Binding { ... })❌ 否绑定逻辑更灵活,适用于运行时动态数据绑定可读性差,不适合大规模开发动态控件生成、运行时绑定、控件库开发
x:Bind(仅 WinUI / UWP)<TextBlock Text="{x:Bind ViewModel.Name}" />❌ 否(编译期绑定)编译期类型检查,性能高,支持函数绑定只能绑定 public 属性,不能继承 DataContextWinUI 中替代 Binding 的主方式

Resources

资源通常定义在资源词典中,也可以定义在很多其他地方,比如页面里,layout里等。 创建资源词典时,放在单独文件里,并在父xaml里引用

template vs style

template 指control的外观,但功能比style强大。 style只能指定缺省外观 template能指定缺省外观,还能指定新外观

trigger

A trigger basically enables you to change property values or take actions based on the value of a property. XAML中的 Trigger是一种在 界面层(View)控制样式和行为的机制,本质上和 ViewModel没有直接关系,它的作用是根据某个属性的值来触发样式变化或动作,属于 UI 响应式逻辑的一部分Trigger是 XAML 中用于“当某个条件满足时,就改变样式或属性”的机制。 有3种trigger

  • Property Triggers: when a change occurs in one property, it will bring either an immediate or an animated change in another property.
  • Data Triggers: it performs some actions when the bound data satisfies some conditions.
  • Event Triggers: it performs some actions when a specific event is fired.

    DataTrigger与 ViewModel 的关系(唯一有关联的)

    你可以这样绑定 ViewModel 的属性: ```xml

🔍 如果 `ViewModel.IsSaving == true`,按钮将禁用,文字变成“Saving…”
✅ 此时 Trigger 依赖 ViewModel 的属性,这种是 `MVVM` 模式推荐用法。
# FAQ
## How to use fontawesome?
nuget > fontawesome.Sharp
![](images/posts/Pasted%20image%2020250628024926.png)
```xml
<Window xmlns:fa="http://schemas.awesome.incremented/wpf/xaml/fontawesome.sharp"> ... </Window>

<fa:IconImage Icon="Book" Width="24" Height="24" Foreground="DeepSkyBlue"></fa:IconImage>

<Button.Content> <fa:IconBlock Icon="Info" Foreground="Chocolate"/> </Button.Content>

<TextBlock />
<IconBlock />
{fa:Icon [Icon]}
{fa:ToText [Icon]}
{fa:Geometry [Icon]}
<fa:IconImage />
{fa:IconSource [Icon]}
<fa:IconToImageConverter />

References

WPF TutorialWindows Presentation Foundation documentation# How to use Font Awesome icon in WPF# FontAwesome.Sharp

WPF Tutorial 1

$
0
0

Introduction

XAML stands for Extensible Application Markup Language. Its a simple and declarative language based on XML.

  • In XAML, it very easy to create, initialize, and set properties of objects with hierarchical relations.
  • It is mainly used for designing GUIs, however it can be used for other purposes as well, e.g., to declare workflow in Workflow Foundation. XAML文件会由专门的解析器处理,编译成内部代码,再与c#代码link并编译,最后生成app。

FAQ

References

[

WPF Tutorial 1

$
0
0

Introduction

MVVM(Model-View-ViewModel)是一种常用于构建用户界面的软件架构模式,特别适用于支持数据绑定的框架,如 WPFXamarinWinUIMAUI等。MVVM 通过分离界面和逻辑,提高了代码的可维护性、可测试性和可重用性。

历史发展

MVVM(Model-View-ViewModel)是在微软推出 WPF 时,为了解决 UI 与逻辑强耦合问题,借鉴 MVC/MVP 等模式而诞生的架构模式。 早期桌面开发:UI 和逻辑混在一起(如 WinForms)。在 WinForms / VB6 / MFC 时代:

  • 所有事件处理都写在按钮点击、窗体加载等事件里
  • UI 和逻辑紧耦合,维护困难,复用性差
  • 你改 UI,可能要改一堆逻辑;你改逻辑,可能要重新布控件
    btnSave.Click+=(s,e)=>{varname=txtName.Text;SaveToDatabase(name);MessageBox.Show("保存成功");};

MVC/MVP 出现:开始尝试分离逻辑和 UI

| 模式 | 说明 | | —————————— | ———————————— | | MVC(Model-View-Controller) | 控制器处理用户输入,更新 Model 和 View(如 ASP.NET) | | MVP(Model-View-Presenter) | View 是被动的,由 Presenter 来驱动 | 这些适合 Web,但在桌面(特别是数据双向绑定)上仍然繁琐:

  • Presenter 仍然需要写很多 UI 更新代码(textBox.Text = model.Name

WPF 2006年发布,催生 MVVM。MVVM 是微软为配合 WPF 的数据绑定系统而设计的架构模式

| 特点 | 原因 | | ———————- | ———— | | WPF 支持强大的数据绑定系统 | 不再需要手动操作控件 | | 控件属性可直接绑定 ViewModel 属性 | 需要一套更清晰的分层设计 | MVVM 的结构

| 层 | 职责 | | ————- | ————————————- | | Model | 数据结构、业务规则 | | View | 纯 UI,使用 XAML | | ViewModel | 暴露属性/命令供 View 绑定,处理 UI 逻辑,不直接操作控件 | WPF使用内建的绑定系统来完成绑定,自动调用,自动更新

MVVM的发展(跨平台 + 框架)

| 阶段 | 演进 | | ——————————- | —————————————- | | .NET MVVM 初期 | 手写 INotifyPropertyChanged、RelayCommand | | MVVM Light、Caliburn.Micro、Prism | 提供简化 ViewModel 编写的工具和导航、消息通信 | | Xamarin.Forms / MAUI | 将 MVVM 用于跨平台移动开发 | | .NET Community Toolkit MVVM | 微软官方支持的现代 MVVM 框架,轻量、现代、Source Generator |

我的总结, chatgpt认可

MVVM是一套框架设计模式。他的设计目标是,实现UI和ViewModel的解耦,而且还能实现两者的双向同步,提升开发效率和可维护性。

MVVM改进了MVC, MVP框架,去除了 Controller/Presenter对UI的直接操作,利用绑定系统和通知机制自动同步状态。他综合了多个设计模式,比如观察者模式(属性通知),命令模式(行为封装),工厂模式等设计出来的。

WPF是MVVM的实现,提供了内建的一系列机制来实现这个目标。

Binding 系统(数据绑定) ICommand(命令绑定) DependencyProperty(支持变更通知),就是写一个类,封装了某一个数据的值和操作方法,然后把这个类注册到WPF框架 DataTemplate - 模板化的view

View通过绑定,将属性和事件绑定到ViewModel。

绑定属性:Text=”{Binding Name}” → 自动连接 ViewModel 的属性。 绑定命令:Command=”{Binding SaveCommand}” → 自动连接 ViewModel 的命令逻辑。

View通过命令模式解耦,在WPF框架帮助下执行业务逻辑。

View 不再直接调用方法,而是绑定到实现了 ICommand 的对象(命令模式)。WPF 框架负责帮你在点击按钮时自动调用 Command.Execute()。

ViewModel通过delegate事件机制,将数据变化通知给WPF框架,由WPF绑定系统负责自动更新UI,实现与UI的解耦。

View知道ViewModel。但ViewModel不知道UI。

View 通过 DataContext = new ViewModel() 明确引用 ViewModel ViewModel 完全不知道 UI 的存在 —— 这样就能进行 单元测试、重用、抽离逻辑

MVVM 的三大组成部分

| 角色 | 作用说明 | | ————- | —————————————————————————————————– | | Model | 表示应用程序的数据业务逻辑。通常是实体类、服务类、数据库访问、API 调用等。不依赖 UI,纯粹面向业务。 | | View | 表示界面(UI),通常是 XAML 文件(如 MainWindow.xaml)。通过数据绑定展示 ViewModel 提供的数据。不包含业务逻辑。 | | ViewModel | 是 View 和 Model 的桥梁。负责数据转换、命令实现、通知 View 更新。ViewModel 不依赖 View,也不知道具体哪个View,他提供供 View 绑定的属性和命令。 | | | |

MVVM 的优点

✅ 清晰分离 UI 和逻辑,降低耦合
✅ 提高测试性(可为 ViewModel 编写单元测试)
✅ 便于多人协作(UI 和逻辑可以分工)
✅ 支持 UI 重用和主题切换

WPF 的三大核心特性决定了MVVM

1. 数据绑定(Data Binding)

  • WPF View(XAML)可直接绑定 ViewModel 的属性。
  • 不需要 Controller 手动操作 UI。

    ✅ 2. 命令系统(ICommand)

  • WPF 提供 ICommand接口,代替传统的事件处理器。
  • ViewModel 可定义命令,View 绑定即可。

    ✅ 3. 通知机制(INotifyPropertyChanged)

  • 支持 UI 随数据变化自动更新。

    主要概念

    数据绑定(Data Binding)

View 通过绑定 ViewModel 的属性(如 Text="{Binding UserName}")自动显示数据。 所谓绑定就是把自己注册到订阅列表里

属性通知(INotifyPropertyChanged)

这是一个接口,表示“我会在属性变更时通过事件通知外部” ViewModel 实现 INotifyPropertyChanged接口,属性值变更时通知 UI 自动更新。 https://github.com/microsoft/referencesource/blob/main/System/compmod/system/componentmodel/INotifyPropertyChanged.cs

publicinterfaceINotifyPropertyChanged{/// <devdoc>/// </devdoc>eventPropertyChangedEventHandlerPropertyChanged;}

https://github.com/microsoft/referencesource/blob/main/System/compmod/system/componentmodel/PropertyChangedEventHandler.cs

publicdelegatevoidPropertyChangedEventHandler(objectsender,PropertyChangedEventArgse);

例如:

publicclassPersonViewModel:INotifyPropertyChanged{privatestring_name;publicstringName{get=>_name;set{if(_name!=value){_name=value;// 事件发生,调用内部方法进行处理OnPropertyChanged(nameof(Name));}}}// 定义一个delegate引用,还没有赋值。由外部+或-来订阅publiceventPropertyChangedEventHandler?PropertyChanged;// 在Windows form中,control本身包括UI和代码2部分,事件由代码处理并负责手动更新UI// 在wpf中,代码负责处理事件,并依次调用外部subscribers注册过的方法。代码不负责更新UIprotectedvoidOnPropertyChanged(stringpropName)=>PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(propName));}

WPF 的事件绑定 vs WinForms 的区别

|项目|WPF / MVVM|Windows Forms| |—|—|—| |数据绑定方式|通过 INotifyPropertyChangedBinding实现属性与 UI 的同步|通常需要手动设置控件值,如 textBox1.Text = person.Name;| |使用 delegate 的方式|事件用于通知绑定系统更新 UI|事件用于响应 UI 行为(如 Click)| |事件机制|event + delegate + INotifyPropertyChanged|event + delegate直接用于事件回调| |控件绑定|支持双向自动绑定,如 {Binding Name}|需要自己写事件处理器来同步数据| |用途|逻辑层 ViewModel -> View 的通知机制|交互层 View -> Logic 的事件处理机制|

绑定系统 (Binding system)

绑定系统(Binding System)是 WPF 框架内部的“监听+同步”引擎: 它负责:

  • 查找 Binding 的数据源(DataContext)
  • 创建 BindingExpression
  • 订阅 INotifyPropertyChanged事件
  • 当你调用 OnPropertyChanged(...)时,自动更新 UI 控件

你在 ViewModel 里定义的这个事件: public event PropertyChangedEventHandler? PropertyChanged; WPF 内部的绑定系统会做一系列事情。 包括绑定属性值,自动注册一个处理函数到PropertyChanged事件上(+=)等等

当你调用 .Invoke()触发事件时,它就会运行那个处理函数,告诉系统:“这个属性值变了!” 然后 WPF 框架就去更新 UI 了

绑定系统(Binding System)是 MVVM 的核心

没有“绑定系统”,就没有 MVVM。

| 原因 | 说明 | | —————– | ——————————————————————– | | 🔗 解耦 UI 与逻辑 | View 不再手动调用 ViewModel,只需绑定表达式 | | 🔄 自动同步状态 | 属性值一变,UI 自动更新;UI 输入,数据自动同步 | | 📉 替代事件、赋值、回调 | View不需要写 Button.Click += ...
后台代码不需要写 textBox.Text = vm.Name | | 🤖 命令绑定代替逻辑控制 | 不再在 code-behind 写事件,统一用命令触发 | 绑定系统的核心技术包括

  • delegate
  • ICommand(行为命令接口)
  • 反射(访问属性值) 绑定系统通过反射找 ViewModel 中的属性,比如 NameSaveCommand
  • 依赖属性(DependencyProperty,View 控件层面的核心)所有 WPF 控件的属性(如 TextBox.Text)都必须是依赖属性,才能绑定和响应变化
  • BindingExpression (内部对象管理源、路径、模式(OneWay/TwoWay)、更新方向等)
  • 值转换器(IValueConverter)在绑定路径中进行格式转换
  • UpdateSourceTrigger 控制何时触发数据源更新(LostFocus、PropertyChanged)

| 技术 | 是否绑定系统核心? | 作用 | | ——————– | ————- | ——————————- | | ✅ delegate | ✅ 是(事件通知机制核心) | 通知属性值变化(INotifyPropertyChanged) | | ✅ 反射 | ✅ 是 | 用反射定位属性路径
找到属性 getter/setter | | ✅ DependencyProperty | ✅ 是 | 控件能被绑定的基础 | | ✅ ICommand | ✅ 是 | 控件行为与逻辑解耦 | | ✅ BindingExpression | ✅ 是 | 绑定表达式对象,管理绑定行为 |

完整绑定触发链(双向绑定示例)

<TextBox Text="{Binding Name, Mode=TwoWay}" />

→ UI 输入数据:

  1. 用户输入 TextBox
  2. 触发 DependencyProperty 变更 → 调用 BindingExpression.UpdateSource()
  3. 通过反射写入ViewModel的 Name属性 → 同时触发ViewModel的 PropertyChanged 事件
  4. 若绑定了多个控件,会触发其它控件同步更新

    → ViewModel 改数据:

  5. 设置 Name属性并触发 PropertyChanged
  6. 绑定系统收到事件 → 调用 BindingExpression.UpdateTarget()
  7. 将新值写入 TextBox 绑定系统负责维护其中的复杂关系

    具体绑定流程

    开发者写: `<TextBox Text="{Binding Name}" />`
           ↓
     Binding 对象(绑定描述)
           ↓   BindingExpression(绑定执行对象)
           ↓ 绑定系统监听 ViewModel 的 Name 属性
           ↓    ViewModel 属性发生变化
           ↓   触发 PropertyChanged 事件
           ↓ BindingExpression 捕捉事件并更新 UI
    

Binding 系统组件总览

组件说明
Binding表示绑定描述(路径、源、模式等)。配置谁绑定谁。
相当于快递单,描述
SetBinding()注册绑定关系到依赖属性。
相当于注册快递单
BindingExpression表示“某一个绑定关系”的执行体。执行绑定,双向监听变更,负责更新。
相当于快递员,具体送货
DataBindEngine内部绑定系统的总控中心(单例),负责所有绑定操作的调度。
相当于物流公司
PropertyPathWorker解析绑定路径如 "User.Address.City"
INotifyPropertyChangedViewModel 触发属性变更事件,驱动更新。ViewModel 通知绑定系统,数据已更改
PropertyChangedViewModel的属性变更事件。
相当于电话通知(货到了)
DependencyProperty控件属性系统,用于接收绑定值,支持绑定和回调。
相当于收件地址(控件属性)

下面逐句分析

<TextBoxText="{Binding Name}"/>

WPF 运行时等价于执行:

var binding = new Binding("Name");
textBox.SetBinding(TextBox.TextProperty, binding);

WPF 中的 Binding是一个类:System.Windows.Data.Binding它表示“控件的某个属性”如何与“数据源的某个属性”建立关系。

FrameworkElement.SetBinding(…) 传入控件本身、目标依赖属性、Binding描述对象

publicBindingExpressionBaseSetBinding(DependencyPropertydp,BindingBasebinding){returnBindingOperations.SetBinding(this,dp,binding);}

BindingOperations.SetBinding(…)

publicstaticBindingExpressionBaseSetBinding(DependencyObjecttarget,DependencyPropertydp,BindingBasebinding){returnbinding.ProvideValueInternal(...);// 简化解释}

这里调用了 Binding.ProvideValue(...),最终会创建一个 BindingExpression对象 BindingExpression是绑定关系的执行者,负责监听源、更新目标。

BindingExpression.Attach(…) 执行以下事情:

  • 确定绑定源(默认是 DataContext
  • PropertyPathWorker解析路径(比如 User.Name
  • 注册到源对象的 PropertyChanged事件上
  • 将源值写入目标 DependencyProperty

注意BindingExpression实现了一个 PropertyChanged监听器,负责监听属性变化

publicclassBindingExpression:BindingExpressionBase{privatevoidOnPropertyChanged(objectsender,PropertyChangedEventArgse){if(e.PropertyName==Path){UpdateTarget();}}}

当你在 ViewModel 中这样写:

_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name));

如果有属性变化,BindingExpression被触发,调用 UpdateTarget()把新值写到 UI 控件的 TextProperty上。

UI 更新:DependencyProperty 系统 控件属性不是普通字段,而是:

publicstaticreadonlyDependencyPropertyTextProperty=...

这些 DependencyProperty提供能力:

  • 能被绑定
  • 能被监听
  • 能在值改变时通知控件刷新

    Binding 的核心类原理剖析

    Binding 它只负责“描述绑定”,不负责执行。

    publicclassBinding:BindingBase{publicstringPath{get;set;}// 要绑定的属性路径publicobjectSource{get;set;}// 显式设置的数据源(可选)publicBindingModeMode{get;set;}// OneWay、TwoWay 等publicUpdateSourceTriggerUpdateSourceTrigger{get;set;}// 更新时机...}

    BindingExpression 是绑定系统的“执行体”,负责监听、更新、转换。

    publicclassBindingExpression:BindingExpressionBase{privateobject_dataItem;// 数据源对象privatePropertyPathWorker_pathWorker;publicoverridevoidUpdateTarget(){...}publicoverridevoidUpdateSource(){...}}

    DataBindEngine

是绑定系统的总控中心:

  • 负责调度表达式
  • 管理所有的活动绑定(弱引用方式)
  • 执行绑定验证、回调、延迟更新等任务

    命令绑定(ICommand)

    用于绑定按钮等 UI 事件到 ViewModel 中的方法。 命令绑定是一种机制,允许你在 XAML 中把按钮等控件的操作(如点击)绑定到 ViewModel 中的“命令对象”,而不需要写事件处理器。 命令绑定把解决了UI控件和逻辑的强耦合:btnSayHello += ….。把命令剥离出来写成单独类,实现了复用,而且也使得UI 和 ViewModel能分别进行测试。

    ICommand 接口定义

    ```csharp public interface ICommand { bool CanExecute(object? parameter); // 是否可以执行,如按钮是否启用 void Execute(object? parameter); // 执行命令 event EventHandler? CanExecuteChanged; // 可执行状态发生变化时通知(delegate) }

这就是命令模式的典型接口,三个要点:

1. **Execute**:真正执行的动作    
2. **CanExecute**:判断是否允许执行(比如表单未填完整 → 按钮禁用)    
3. **CanExecuteChanged**:通知系统重新评估 `CanExecute`(触发按钮启用/禁用变化)

它用了什么设计模式?

|名称|是否体现|原因|
|---|---|---|
|✅ 命令模式|✔|将“动作”封装为对象(如 SaveCommand),可绑定到 UI|
|✅ 委托(delegate)|✔|用事件 `CanExecuteChanged` 通知控件状态更新|
|✅ 观察者模式|✔|控件监听命令的 `CanExecuteChanged`,当命令状态变化就更新|

### 命令绑定(ICommand)的核心意义
| 功能          | 实现方式                            |
| ----------- | ------------------------------- |
| 把操作抽象成对象    | 用 ICommand 接口                   |
| 控件自动绑定并调用   | Command="{Binding SaveCommand}" |
| 控制按钮启用/禁用   | 实现 CanExecute()                 |
| 解耦 View 和逻辑 | 逻辑完全放在 ViewModel 中              |
| 支持 MVVM     | 是 WPF 支持 MVVM 的关键机制之一           |
### 流程
+---------+       +------------------+
| Button  |-----> |   ICommand       | <-------+
| (Invoker)|      | + Execute()      |         |
+---------+       | + CanExecute()   |         |
                  +------------------+         |
                             ^                 |
                     +---------------+         |
                     | RelayCommand  |         |
                     +---------------+         |
                             ^                 |
                     +----------------+        |
                     |   ViewModel    |--------+
                     +----------------+

例如

> 这里,View背后绑定到了ViewModel中的SayHelloCommand属性
> View 只绑定 `Command="{Binding SaveCommand}"`
> 不需要知道 SayHelloCommand 背后执行了什么

创建Command对象
```csharp
public class RelayCommand : ICommand
{
    private readonly Action _execute;
    public RelayCommand(Action execute) => _execute = execute;
    public bool CanExecute(object? parameter) => true;
    public void Execute(object? parameter) => _execute();
    public event EventHandler? CanExecuteChanged;
}

ViewModel 中使用:

publicICommandSayHelloCommand{get;}publicMyViewModel(){SayHelloCommand=newRelayCommand(()=>MessageBox.Show("Hello!"));}

当你点击按钮时,WPF 会自动执行 SayHelloCommand.Execute(),不需要你在 Button_Click里写代码。

ICommand命令绑定 vs Data Binding数据绑定

ICommand命令绑定 和 WPF 的 数据绑定系统都是通过“绑定表达式(BindingExpression)”结合在一起的。 数据绑定只用到了delegate和观察者模式,命名绑定又加了命令模式,显得复杂了些。 命令绑定(Command=”{Binding XXXCommand}”)也是使用 WPF 的数据绑定系统来连接控件与 ViewModel 的 ICommand 对象,只不过绑定目标是 ICommand 类型而不是普通属性。 WPF 框架系统在合适的时间自动调用命令绑定的 CanExecute()Execute()方法,不需要自己调用。

|类比角色|普通属性绑定|命令绑定| |—|—|—| |UI控件属性|TextBox.Text|Button.Command| |数据源属性|string Name|ICommand SaveCommand| |Binding系统|Binding("Name")|Binding("SaveCommand")| |控件行为|自动更新 Text|自动调用 Execute()|

命令绑定 ≈ 数据绑定的一种特殊形式

普通数据绑定 绑的是属性

<TextBox Text="{Binding Name}" />
  • 绑定目标是 DependencyProperty:Text
  • 绑定源是 ViewModel 的属性:Name(string) 命令绑定 绑的是行为 ```

- 绑定目标是 `DependencyProperty`:Button.Command(类型为 `ICommand`)    
- 绑定源是 ViewModel 的属性:SaveCommand(类型为 `ICommand`)
- ✅ 本质上,`Command="{Binding SaveCommand}"` 也是一个 `BindingExpression`!
### 命令绑定的执行过程
1. XAML 中写下 `Command="{Binding SaveCommand}"`    
2. WPF 调用 `SetBinding(Button.CommandProperty, binding)    
3. 绑定系统创建 `BindingExpression`,找到了 ViewModel 的 `SaveCommand` 属性    
4. `Button.CommandProperty` ← 绑定了这个 `ICommand` 对象(比如 `RelayCommand` 实例)    
5. Button 控件内部:    
    - 在加载和状态变化时调用 `command.CanExecute()` 决定是否启用        
    - 在用户点击时调用 `command.Execute()`
## 命令绑定 vs 属性通知 

命令绑定实现了命令设计模式。属性通知实现了观察者模式

| 项目           | 命令模式(Command)              | 观察者模式(Observer)                             |
| ------------ | -------------------------- | ------------------------------------------- |
| 用于控制行为       | ✅ 是                        | ❌ 否                                         |
| 用于同步数据       | ❌ 否                        | ✅ 是                                         |
| MVVM 中的实现    | `ICommand`, `RelayCommand` | `INotifyPropertyChanged`, `PropertyChanged` |
| 控件角色         | 调用命令                       | 观察 ViewModel 数据变化                           |
| ViewModel 作用 | 暴露命令供绑定                    | 实现通知机制供绑定                                   |
2个模式用来解决不同问题

|模式|解决的问题|MVVM 中的应用|
|---|---|---|
|✅ **命令模式**(Command Pattern)|**将请求封装成对象,解耦请求发送者与执行者**解决“按钮点击时要执行什么操作”的问题|WPF 中的 `ICommand` 和 `RelayCommand`比如:`Button.Command="{Binding SaveCommand}"`控件不需要知道逻辑怎么写|
|✅ **观察者模式**(Observer Pattern)|**当一个对象状态改变时,自动通知依赖它的多个对象**解决“属性值变了,UI 如何自动更新”的问题|ViewModel 实现 `INotifyPropertyChanged`WPF 绑定系统监听这个事件比如:`OnPropertyChanged("Name")` 通知 UI 刷新绑定的 `TextBox.Text`|
换个角度

| MVVM 层           | 使用的设计模式 | 用途               |
| ---------------- | ------- | ---------------- |
| ViewModel → View | 观察者模式   | 数据变 → 通知 UI 自动更新 |
| View → ViewModel | 命令模式    | 用户操作 → 调用命令方法    |
# 支持库
| 框架/库名                                     | 发布年份  | 作者/主导           | 适用平台                    | 特点关键词                      | 当前状态               |
| ----------------------------------------- | ----- | --------------- | ----------------------- | -------------------------- | ------------------ |
| **Prism**                                 | ~2008 | 微软 → 社区         | WPF, UWP, Xamarin, MAUI | 模块化、导航、事件聚合、DI             | ✅ 活跃维护             |
| **MVVM Light**                            | ~2009 | Laurent Bugnion | WPF, UWP, Xamarin       | 轻量、Messenger、RelayCommand  | ❌ 停止维护(作者加入微软)     |
| **Caliburn.Micro**                        | ~2010 | Rob Eisenberg   | WPF, UWP                | 约定优于配置、自动绑定、Action 调用      | ⚠️ 基本停滞(维护慢)       |
| **ReactiveUI**                            | ~2010 | 社区              | 所有 .NET UI 平台           | 响应式、Rx.NET、双向绑定            | ✅ 活跃维护(偏 Rx 思维)    |
| **CommunityToolkit.Mvvm**(原 MVVM Toolkit) | 2020  | 微软官方            | WPF, WinUI, MAUI, Uno   | Source Generator、属性注解、命令注解 | ✅ 官方推荐             |
| **FreshMvvm**                             | ~2016 | Michael Ridland | Xamarin.Forms           | Page 自动注入、导航集成             | ⚠️ Xamarin 退场,维护有限 |
| **Catel**                                 | ~2012 | 社区              | WPF, Xamarin            | MVVM + DI + Validation     | ⚠️ 小众,维护减缓         |
| **Template10**                            | ~2015 | Jerry Nixon(微软) | UWP                     | UWP 快速开发框架                 | ❌ 已弃用(微软也不再推荐)     |
| **Stylet**                                | ~2017 | 个人/小团队          | WPF                     | 类似 Caliburn.Micro,精简、可测试   | ⚠️ 小众,文档少          |
Prism vs ReactiveUI vs MVVM Toolkit 对比表

| 比较维度                     | **Prism**                         | **ReactiveUI**                                    | **MVVM Toolkit** (CommunityToolkit.Mvvm)     |
| ------------------------ | --------------------------------- | ------------------------------------------------- | -------------------------------------------- |
| 🔰 发布年份                  | ~2008                             | ~2010                                             | 2020(.NET 统一平台后)                             |
| 🧠 核心理念                  | 企业级模块化、导航、DI、事件聚合                 | 响应式编程、数据流驱动                                       | 极简、自动生成样板代码                                  |
| 🧩 是否完整框架                | ✅ 是,功能齐全                          | ❌ 否,偏工具库(基于 Rx.NET)                               | ❌ 否,仅提供属性和命令的精简实现                            |
| 💡 是否官方支持                | 最初微软,后转社区维护                       | ❌ 社区主导                                            | ✅ 微软官方维护                                     |
| 🛠️ 平台支持                 | ✅ WPF, UWP, Xamarin, MAUI         | ✅ 全平台(WPF, WinUI, MAUI, Console)                  | ✅ WPF, WinUI, MAUI, Uno                      |
| 🪝 依赖注入(DI)支持            | ✅ 内建支持(Unity, DryIoc 等)           | ❌ 需自行接入                                           | ❌ 手动注入                                       |
| 🧭 导航系统支持                | ✅ Prism Regions                   | ❌ 无导航支持                                           | ❌ 无导航系统                                      |
| 📣 消息通信系统                | ✅ EventAggregator                 | ✅ MessageBus                                      | ✅ WeakReferenceMessenger                     |
| 🔗 命令支持                  | `DelegateCommand`                 | `ReactiveCommand`                                 | `[RelayCommand]` 注解生成                        |
| 🧮 属性绑定支持                | `BindableBase.SetProperty()`      | `this.RaiseAndSetIfChanged()`                     | `[ObservableProperty]` 注解生成                  |
| ⚡ 异步命令                   | ✅ 支持 Task 封装命令                    | ✅ 原生支持异步命令                                        | ✅ 支持 async 方法自动生成命令                          |
| 🧪 测试友好度                 | ✅ 强(ViewModel 易 Mock)             | ✅ 极强(Rx 可完全测试)                                    | ✅ 强(自动生成命令和属性可测试)                            |
| 🧶 使用复杂度                 | ⚠️ 中等偏高(依赖结构和导航配置)                | ⚠️ 高(需熟悉 Rx 思维)                                   | ✅ 低(非常简单直观)                                  |
| 🧰 命令/属性语法               | 手动 `new DelegateCommand()` / 手动通知 | `ReactiveCommand.Create()` / RaiseAndSetIfChanged | 纯注解 `[RelayCommand]`, `[ObservableProperty]` |
| 🧬 是否使用 Source Generator | ❌ 否                               | ❌ 否                                               | ✅ 是(属性/命令全自动生成)                              |
| ✨ ViewModel 基类           | `BindableBase`                    | `ReactiveObject`                                  | `ObservableObject`                           |
| 📦 NuGet 包名              | `Prism.*`                         | `ReactiveUI.*`                                    | `CommunityToolkit.Mvvm`                      |
| 📚 学习曲线                  | 中等偏高(结构繁杂)                        | 高(响应式概念较陡峭)                                       | 低(面向初学者友好)                                   |
| 🎯 推荐适用场景                | 企业级、模块化应用、WPF/MAUI 大项目            | 高响应性、复杂交互、函数式编程爱好者                                | 现代 .NET 项目、WPF/MAUI 普通 MVVM 项目               |
# Develop
`INotifyCollectionChanged`接口:集合的话需要实现INotifyCollectionChanged 接口  ,会一个事件叫:NotifyCollectionChangedEventHandler 

这个事件作用是:是当集合改变时会发生响应,从而会提供一个`ObservableCollection<T>` 动态数据集合
属性的话是需要继承一个INotifyPropertyChanged接口,会提供一个事件叫:PropertyChangedEventHandler  
这个事件作用是:在更改属性值时会发生响应

同时页面需要在绑定字段的时候设置监听属性:UpdateSourceTrigger=PropertyChanged  mode=TwoWay 获取或设置一个值,该值指示绑定的数据流方向。
# FAQ
## MVVM vs MVC vs MVP
如果没有绑定系统,MVVM 退化成 MVP 或 MVC,你就得写:

textBox.Text = viewModel.Name; viewModel.Name = textBox.Text; button.Click += (_, __) => viewModel.Save();

这让 View 直接控制逻辑,完全违背 MVVM 的目标(分离 UI 和逻辑)。

|比较项|MVC 特点|WPF 需求|冲突点|
|---|---|---|---|
|**View 与 Controller 的交互方式**|View 通过事件通知 Controller,Controller 决定更新 View|WPF 支持双向数据绑定,View 会自动响应数据变化|WPF 不需要 Controller 主动更新 View|
|**UI 更新方式**|控制器调用 View 方法手动更新界面|View 绑定 ViewModel 属性,自动刷新|控制器逻辑违背 WPF 的自动更新机制|
|**耦合度**|Controller 通常知道 View 的细节|WPF 倡导松耦合,ViewModel 不知道 View|MVC 的紧耦合不适合数据绑定|
|**事件驱动 vs 绑定驱动**|MVC 以事件回调为主|WPF 以绑定驱动为主|设计理念根本不同|
假设你有个按钮点击后,更新一个 Label 的文本。
MVC
```csharp
public class Controller {
    public void OnButtonClick() {
        var data = model.GetData();
        view.SetLabelText(data); // 控制器操作 UI
    }
}

这里,Controller 必须直接操纵 View 控件,破坏了 WPF 的绑定思想。

在 WPF + MVVM 中:

// View.xaml<ButtonCommand="{Binding LoadCommand}"/><TextBlockText="{Binding Message}"/>// ViewModel.cspublicICommandLoadCommand=>newRelayCommand(()=>Message=model.GetData());publicstringMessage{get;set;}// 通知 UI 自动更新

这里,WPF 自动完成数据更新。ViewModel 根本不需要知道 UI 是什么控件,UI 根本不需要知道谁负责处理我的事件

总之,WPF 的设计理念是绑定驱动的声明式 UI,而 MVC 是事件驱动的命令式逻辑,两者冲突明显。所以 MVVM 才是 WPF 的“原生”架构模式。

下面是一个完整例子。分别用WPF方式实现MVC和MVVM模式

用WPF模拟MVC

model.cs

publicclassMessageModel{publicstringGetMessage()=>"你好,WPF!";}

MainWindow.xaml (界面View)

<Windowx:Class="MvcDemo.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="MVC Demo"Height="150"Width="300"><StackPanelMargin="20"><ButtonName="btnShow"Content="显示消息"Margin="0 0 0 10"/><TextBlockName="lblMessage"FontSize="16"/></StackPanel></Window>

MainWindow.xaml.cs

publicpartialclassMainWindow:Window{privateMessageControllercontroller;publicMainWindow(){InitializeComponent();controller=newMessageController(this);}}

Controller.cs

publicclassMessageController{privatereadonlyMessageModelmodel;privatereadonlyMainWindowview;publicMessageController(MainWindowview){this.view=view;this.model=newMessageModel();this.view.btnShow.Click+=OnButtonClick;}privatevoidOnButtonClick(objectsender,RoutedEventArgse){stringmessage=model.GetMessage();view.lblMessage.Text=message;// 直接操作 UI}}

可以运行,但 Controller 强依赖 View 结构,无法单元测试、无法复用,也违背了 WPF 的绑定哲学。

用WPM实现MVP

MessageModel.cs(保持不变)

public class MessageModel
{
    public string GetMessage() => "你好,WPF!";
}

MainWindow.xaml(与mvc一样)

<Window x:Class="MvpDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MVP Demo" Height="150" Width="300">
    <StackPanel Margin="20">
        <Button Name="btnShow" Content="显示消息" Margin="0 0 0 10" />
        <TextBlock Name="lblMessage" FontSize="16" />
    </StackPanel>
</Window>

IMessageView.cs(View 接口)

public interface IMessageView
{
    void SetMessage(string message);
    event RoutedEventHandler ShowButtonClicked;
}

MainWindow.xaml.cs(实现接口)

publicpartialclassMainWindow:Window,IMessageView{privateMessagePresenterpresenter;publicMainWindow(){InitializeComponent();presenter=newMessagePresenter(this);}publicvoidSetMessage(stringmessage){lblMessage.Text=message;}publiceventRoutedEventHandlerShowButtonClicked{add{btnShow.Click+=value;}remove{btnShow.Click-=value;}}}

MessagePresenter.cs

publicclassMessagePresenter{privatereadonlyMessageModelmodel=new();privatereadonlyIMessageViewview;publicMessagePresenter(IMessageViewview){this.view=view;this.view.ShowButtonClicked+=OnShowClicked;}privatevoidOnShowClicked(objectsender,RoutedEventArgse){stringmessage=model.GetMessage();view.SetMessage(message);// 不直接操作控件}}

MVC vs MVP 对比

|比较点|MVC|MVP| |—|—|—| |控制器操作|直接操作控件|通过接口控制视图| |解耦程度|Controller ←→ View 耦合|Presenter ←→ View 解耦(靠接口)| |可测试性|较弱|强:Presenter 可单元测试| |UI 变化影响|Controller 需改动|View 接口不变可复用 Presenter|

用WPF实现MVVM

MessageModel.cs(保持不变)

publicclassMessageModel{publicstringGetMessage()=>"你好,WPF!";}

ViewModel.cs

publicclassMessageViewModel:INotifyPropertyChanged{privatereadonlyMessageModelmodel=new();privatestring_message=string.Empty;publicstringMessage{get=>_message;set{if(_message!=value){_message=value;OnPropertyChanged(nameof(Message));}}}publicICommandShowMessageCommand=>newRelayCommand(()=>{Message=model.GetMessage();// 只修改属性,不直接操作 UI});publiceventPropertyChangedEventHandler?PropertyChanged;protectedvoidOnPropertyChanged(stringprop)=>PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(prop));}

RelayCommand.cs(命令帮助类)

publicclassRelayCommand:ICommand{privatereadonlyAction_execute;publicRelayCommand(Actionexecute)=>_execute=execute;publicboolCanExecute(object?parameter)=>true;publicvoidExecute(object?parameter)=>_execute();publiceventEventHandler?CanExecuteChanged;}

MainWindow.xaml (View) 绑定命令和属性

<Windowx:Class="MvvmDemo.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="MVVM Demo"Height="150"Width="300"><StackPanelMargin="20"><ButtonContent="显示消息"Command="{Binding ShowMessageCommand}"Margin="0 0 0 10"/><TextBlockText="{Binding Message}"FontSize="16"/></StackPanel></Window>

MainWindow.xaml.cs

publicpartialclassMainWindow:Window{publicMainWindow(){InitializeComponent();DataContext=newMessageViewModel();// 绑定 ViewModel}}

✅ View 只绑定 ViewModel,不知道 Model
✅ ViewModel 不知道 UI 控件,容易测试
✅ Model 是独立逻辑层,可复用

MVC / MVP / MVVM 文件结构对比

| 文件名 | MVC | MVP | MVVM | | ——————– | —– | —- | ————- | | Model.cs | ✅ | ✅ | ✅ | | View.xaml | ✅ | ✅ | ✅(绑定) | | View.xaml.cs | 控制器绑定 | 实现接口 | ❌ 无逻辑 | | Controller/Presenter | ✅ | ✅ | ❌ ViewModel代替 | | ViewModel.cs | ❌ | ❌ | ✅ | | RelayCommand.cs | ❌ | ❌ | ✅ 用于命令绑定 | MVC / MVP / MVVM实现效果对比

|比较维度|MVC|MVP|MVVM| |—|—|—|—| |控制方式|Controller|Presenter|ViewModel| |是否依赖事件|✅ 需要|✅ 需要|❌ 不需| |是否手动操作控件|✅ 是|❌ 通过接口|❌ 全自动| |易测试性|❌ 差|✅ 强|✅ 强| |是否支持绑定|❌ 否|❌ 否|✅ 原生支持|

mvvm的设计,用到了哪些经典设计模式

MVVM(Model-View-ViewModel)模式本身并不是一个“设计模式”,而是一个架构模式,但它内部广泛应用了多个经典设计模式来实现松耦合、可测试和易扩展的架构。下面是 MVVM 中常用的设计模式:

| 设计模式 | 用途/在 MVVM 中的角色 | | —————————— | ———————————————————————————– | | Observer(观察者模式) | 用于通知 UI 更新。ViewModel 实现 INotifyPropertyChanged接口,View 观察 ViewModel 属性变化,自动更新 UI。 | | Command(命令模式) | 用于将按钮等 UI 行为转化为命令对象。ICommand接口 + RelayCommand是典型命令模式实现。 | | Mediator(中介者模式) | View 和 ViewModel 通过绑定机制间接通信,不直接依赖彼此,WPF 的 Binding系统本质上是一个中介者。 | | Dependency Injection(依赖注入) | 将服务(如 Model 或 Repository)注入到 ViewModel 中,解耦依赖关系,便于测试和替换。 | | Factory(工厂模式) | ViewModel、Model、服务对象的创建往往使用工厂方法(比如 ViewModelLocator 或 ServiceLocator)。 | | Service Locator(服务定位器) | 可选的模式,用于统一管理服务实例(但滥用会导致隐藏依赖)。某些 MVVM 框架如 Prism 提供此模式支持。 | | Template Method(模板方法模式) | 用于定义数据加载、初始化过程的通用骨架,ViewModel 中常有 BaseViewModel 定义生命周期方法。 | | Strategy(策略模式) | 有时用于在 ViewModel 中切换不同的业务处理方式,比如根据用户权限切换显示策略。 | | State(状态模式) | ViewModel 中的 UI 状态(比如是否加载中、是否只读)可视为状态机模型。 |

RelayCommand是谁写的?

不是 WPF 官方提供的
✅ 是 社区约定俗成的写法你需要自己写一份(或者引用别人写好的) 你如果不用库,就必须写一个 RelayCommandDelegateCommand自己封装它。 RelayCommand基本写法:

publicclassRelayCommand:ICommand{privatereadonlyAction<object?>_execute;privatereadonlyFunc<object?,bool>?_canExecute;publicRelayCommand(Action<object?>execute,Func<object?,bool>?canExecute=null){_execute=execute;_canExecute=canExecute;}publicboolCanExecute(object?parameter)=>_canExecute?.Invoke(parameter)??true;publicvoidExecute(object?parameter)=>_execute(parameter);publiceventEventHandler?CanExecuteChanged;publicvoidRaiseCanExecuteChanged()=>CanExecuteChanged?.Invoke(this,EventArgs.Empty);}

整个项目只写一个 RelayCommand 类(通常放在 Common 或 Infrastructure 文件夹) 然后在任何 ViewModel 中重复使用它: MyViewMode: ```csharp public class MyViewModel { public ICommand SaveCommand { get; }

public MyViewModel()
{
    SaveCommand = new RelayCommand(_ => Save());
}

private void Save()
{
    // 执行保存逻辑
} } ``` 常见Command变体

|命名|来源 / 框架| |—|—| |RelayCommand|MVVM Light / 通用| |DelegateCommand|Prism 框架| |AsyncRelayCommand|支持异步命令执行| |MyCommand|你自己随便起的名字| 总结

|问题|回答| |—|—| |WPF 官方提供 RelayCommand 吗?|❌ 没有,需要自己写| |每个 ViewModel 都要写一份?|❌ 不需要,只写一个,全项目通用| |为什么社区都叫它 RelayCommand?|因为它像“转接器”,帮你把方法转成 ICommand| |推荐用现成框架吗?|✅ 是的,例如 MVVM Toolkit 或 Prism|

References

# MVC、MVP、MVVM模式的概念与区别

.NET Community Toolkit

$
0
0

Introduction

.NET Community Toolkit(简称 Community ToolkitToolkit)是微软和社区联合开发的一个开源库集合,2021年左右基于以前各类微软辅助库的基础上推出。旨在为 .NET 应用开发者提供一组高效、易用且现代化的工具和辅助库,提升开发效率和代码质量。

它主要聚焦于:

  • MVVM 模式支持(主要针对 WPF、WinUI、UWP 等桌面与现代应用)
  • 性能优化(如高性能集合和辅助类)
  • 简化常见任务(如事件处理、消息传递、数据绑定等)

    发展历程:

| 时间 | 事件/版本 | 说明 | | ——— | ———————————- | —————————————– | | 2016-2018 | Windows Community Toolkit | 针对 UWP 的社区工具库,微软主导,包含控件、MVVM、辅助方法等。 | | 2019-2020 | 工具包逐渐成熟,支持多种UWP特性 | 增加更多控件和扩展,社区贡献活跃。 | | 2021年 | 重构成 .NET Community Toolkit | 重命名并开始支持 WinUI 3 和更广泛的 .NET 平台(包括 WPF)。 | | 2021-2023 | 模块化拆分,MVVM Toolkit 独立 | MVVM Toolkit 模块脱离主工具包,成为独立的 NuGet 包,简洁轻量。 | | 2023-2024 | 支持 .NET 6 / 7 / MAUI,加入更多性能优化和扩展集合 | 持续更新,增强性能,支持跨平台(WinUI、WPF、MAUI)。 | | 2025年+ | 持续迭代,聚焦开发者体验和跨平台支持 | 预计将增强与 Blazor 等新兴框架集成,强化开发者工具链。 |

为什么使用MVVC Toolkit?

|问题(过去的 MVVM)|MVVM Toolkit 的解决方案| |—|—| |要手写 INotifyPropertyChanged|✅ 用 [ObservableProperty]自动生成| |要写一堆 RelayCommand|✅ 用 [RelayCommand]自动生成命令| |属性变更还要手动 OnPropertyChanged()|✅ 自动生成通知逻辑| |手写命令类重复代码多|✅ 一个属性 + 属性名 = 自动生成命令| |没有统一风格|✅ 微软官方风格,轻量、现代、跨平台|

Install

NuGet 安装 CommunityToolkit.Mvvm or dotnet add package CommunityToolkit.Mvvm 这是最常用、最核心的 MVVM 模块。其他模块(如 Collections、Diagnostics)可以根据需要添加。

| 模块名称 | 功能简介 | | ———————————- | ———————————————– | | CommunityToolkit.Mvvm | MVVM 支持:ObservableObject, RelayCommand, 消息等 | | CommunityToolkit.Collections | 高性能集合,支持异步分页、线程安全集合等 | | CommunityToolkit.Diagnostics | 更智能的调试辅助,如 Guard | | CommunityToolkit.HighPerformance | 针对 Span/Memory 的性能优化方法 | | CommunityToolkit.WinUI | 针对 WinUI 的 UI 扩展和控件(适用于 WinUI 项目) | MVVM Toolkit 核心功能

|功能|用法例子| |—|—| |自动实现属性通知|[ObservableProperty] string name;→ 自动生成属性和通知| |自动生成命令|[RelayCommand] void Save()→ 自动生成 SaveCommand| |异步命令支持|[RelayCommand] async Task LoadAsync()| |属性变化通知关系|[NotifyPropertyChangedFor(nameof(IsValid))]| |ViewModel 基类|ObservableObject, ObservableRecipient, ObservableValidator| |消息通信(解耦)|IMessenger, WeakReferenceMessenger|

How to use

一个简单的 WPF MVVM 例子,功能是一个文本输入框和一个按钮,点击按钮后弹出输入内容的消息框。

  • 一个 TextBox 双向绑定 UserInput属性
  • 一个按钮绑定 ShowMessageCommand
  • 点击按钮,弹出消息框显示当前 UserInput内容

    传统写法:没有用 .NET Community Toolkit,自己实现 INotifyPropertyChangedICommand

    ViewModel.cs

usingSystem;usingSystem.ComponentModel;usingSystem.Windows.Input;publicclassMainViewModel:INotifyPropertyChanged{privatestring_userInput;publicstringUserInput{get=>_userInput;set{if(_userInput!=value){_userInput=value;OnPropertyChanged(nameof(UserInput));}}}publicICommandShowMessageCommand{get;}publicMainViewModel(){ShowMessageCommand=newRelayCommand(ShowMessage);}privatevoidShowMessage(){System.Windows.MessageBox.Show($"你输入了: {UserInput}");}publiceventPropertyChangedEventHandler?PropertyChanged;protectedvoidOnPropertyChanged(stringpropertyName)=>PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(propertyName));}// ICommand 实现publicclassRelayCommand:ICommand{privatereadonlyAction_execute;privatereadonlyFunc<bool>?_canExecute;publicRelayCommand(Actionexecute,Func<bool>?canExecute=null){_execute=execute??thrownewArgumentNullException(nameof(execute));_canExecute=canExecute;}publicboolCanExecute(object?parameter)=>_canExecute?.Invoke()??true;publicvoidExecute(object?parameter)=>_execute();publiceventEventHandler?CanExecuteChanged;publicvoidRaiseCanExecuteChanged()=>CanExecuteChanged?.Invoke(this,EventArgs.Empty);}

MainWindow.xaml (View)

<Windowx:Class="WpfApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="传统写法"Height="150"Width="300"><StackPanelMargin="20"><TextBoxText="{Binding UserInput, UpdateSourceTrigger=PropertyChanged}"/><ButtonContent="显示消息"Command="{Binding ShowMessageCommand}"Margin="0,10,0,0"/></StackPanel></Window>

MainWindow.xaml.cs

publicpartialclassMainWindow:Window{publicMainWindow(){InitializeComponent();DataContext=newMainViewModel();}}

使用 Community Toolkit:用它自带的 ObservableObjectRelayCommand,代码简洁很多。

ViewModel.cs

usingCommunityToolkit.Mvvm.ComponentModel;usingCommunityToolkit.Mvvm.Input;usingSystem.Windows;publicpartialclassMainViewModel:ObservableObject{[ObservableProperty]privatestringuserInput;publicMainViewModel(){ShowMessageCommand=newRelayCommand(()=>{MessageBox.Show($"你输入了: {UserInput}");});}publicRelayCommandShowMessageCommand{get;}}

注意:

  • [ObservableProperty]特性自动生成 UserInput属性和通知代码
  • 直接用 RelayCommand,不用自己写 ICommand 实现

MainWindow.xaml (View) 不变

<Windowx:Class="WpfApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="传统写法"Height="150"Width="300"><StackPanelMargin="20"><TextBoxText="{Binding UserInput, UpdateSourceTrigger=PropertyChanged}"/><ButtonContent="显示消息"Command="{Binding ShowMessageCommand}"Margin="0,10,0,0"/></StackPanel></Window>

MainWindow.xaml.cs 不变

publicpartialclassMainWindow:Window{publicMainWindow(){InitializeComponent();DataContext=newMainViewModel();}}

对比

|点|传统写法|使用 Toolkit| |—|—|—| |代码量|较多,需要手动写 INotifyPropertyChangedICommand实现|非常简洁,属性和命令通过特性和基类自动生成| |可维护性|代码重复、易错,特别是属性通知|结构清晰,减少样板代码| |学习曲线|理解接口细节较多|更易上手,专注业务逻辑| |性能|好,但写法繁琐|利用 source generators 优化性能| |社区和微软支持|需要自己维护和测试|官方支持,持续更新,社区活跃|

FAQ

References

.NET Community Toolkit source code github# Windows Community Toolkit Documentation

Dependency Injection

$
0
0

Introduction

是的,WPF 本身不内建 DI(依赖注入)框架,但它完全支持依赖注入。 你可以在 App 启动时手动配置并注入服务, 再在窗口或 ViewModel 中通过构造函数注入依赖。 可以使用你喜欢的 DI 容器

  • Microsoft.Extensions.DependencyInjection(官方推荐)
  • Autofac
  • Unity
  • DryIoc 等

    Install

    Install-Package Microsoft.Extensions.DependencyInjection 在 App.xaml.cs 配置容器

    publicpartialclassApp:Application{publicstaticIServiceProviderServiceProvider{get;privateset;}protectedoverridevoidOnStartup(StartupEventArgse){base.OnStartup(e);varserviceCollection=newServiceCollection();ConfigureServices(serviceCollection);ServiceProvider=serviceCollection.BuildServiceProvider();varmainWindow=ServiceProvider.GetRequiredService<MainWindow>();mainWindow.Show();}privatevoidConfigureServices(IServiceCollectionservices){// 注册窗口services.AddSingleton<MainWindow>();// 注册 ViewModelservices.AddTransient<MainViewModel>();// 注册服务services.AddSingleton<IGreetingService,GreetingService>();}}

    使用构造函数注入依赖。示例:MainWindow.xaml.cs

publicpartialclassMainWindow:Window{publicMainWindow(MainViewModelviewModel){InitializeComponent();DataContext=viewModel;}}

服务接口和实现

publicinterfaceIGreetingService{stringGetGreeting();}publicclassGreetingService:IGreetingService{publicstringGetGreeting()=>"你好,WPF with DI!";}

MainViewModel.cs

publicclassMainViewModel{privatereadonlyIGreetingService_greetingService;publicstringMessage=>_greetingService.GetGreeting();publicMainViewModel(IGreetingServicegreetingService){_greetingService=greetingService;}}

FAQ

References

.NET Community Toolkit source code github# Windows Community Toolkit Documentation

Viewing all 113 articles
Browse latest View live