2023年4月

大家好,我是三友~~

前几天有个大兄弟问了我一个问题,注册中心要集成SpringCloud,想实现SpringCloud的负载均衡,需要实现哪些接口和规范。

既然这个兄弟问到我了,而我又刚好知道,这不得好好写一篇文章来回答这个问题,虽然在后面的聊天中我已经回答过了。

接下来本文就以探究一下Nacos、OpenFeign、Ribbon、loadbalancer等组件协调工作的原理的方式,来讲一讲应该需要是实现哪些接口了。

再多说一句,本文并没有详细地深入剖析各个组件的源码,如果有感兴趣的兄弟可以从微信公众号
三友的java日记
后台菜单栏中的文章分类中查看我之前写的关于Nacos、OpenFeign、Ribbon源码剖析的文章。


Nacos

先从Nacos讲起。

Nacos是什么,官网中有这么一段话

这一段话说的直白点就是Nacos是一个注册中心和配置中心!

在Nacos中有客户端和服务端的这个概念

  • 服务端需要单独部署,用来保存服务实例数据的
  • 客户端就是用来跟服务端通信的SDK,支持不同语言

当需要向Nacos服务端注册或者获取服务实例数据的时候,只需要通过Nacos提供的客户端SDK就可以了,就像下面这样:

引入依赖

<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>1.4.4</version>
</dependency>

示例代码

Properties properties = new Properties();
properties.setProperty("serverAddr""localhost");
properties.setProperty("namespace""8848");

NamingService naming = NamingFactory.createNamingService(properties);

//服务注册,注册一个order服务,order服务的ip是192.168.2.100,端口8080
naming.registerInstance("order""192.168.2.100"8080);

//服务发现,获取所有的order服务实例
List<Instance> instanceList = naming.selectInstances("order"true);

当服务注册到Nacos服务端的时候,在服务端内部会有一个集合去存储服务的信息

这个集合在注册中心界中有个响亮的名字,
服务注册表


如何进行服务自动注册?

用过SpringCloud的小伙伴肯定知道,在项目启动的时候服务能够自动注册到服务注册中心,并不需要手动写上面那段代码,那么服务自动注册是如何实现的呢?


服务自动注册三板斧

SpringCloud本身提供了一套服务自动注册的机制,或者说是约束,其实就是三个接口,只要注册中心实现这些接口,就能够在服务启动时自动注册到注册中心,而这三个接口我称为服务自动注册三板斧。


服务实例数据封装--Registration

Registration是SpringCloud提供的一个接口,继承了ServiceInstance接口

Registration
Registration
ServiceInstance
ServiceInstance

从ServiceInstance的接口定义可以看出,这是一个服务实例数据的封装,比如这个服务的ip是多少,端口号是多少。

所以Registration就是当前服务实例数据封装,封装了当前服务的所在的机器ip和端口号等信息。

Nacos既然要整合SpringCloud,自然而然也实现了这个接口

NacosRegistration
NacosRegistration

这样当前服务需要被注册到注册中心的信息就封装好了。


服务注册--ServiceRegistry

ServiceRegistry也是个接口,泛型就是上面提到的服务实例数据封装的接口

ServiceRegistry
ServiceRegistry

这个接口的作用就是把上面封装的当前服务的数据Registration注册通过
register
方法注册到注册中心中。

Nacos也实现了这个接口。

NacosServiceRegistry
NacosServiceRegistry

并且核心的注册方法的实现代码跟前面的demo几乎一样


服务自动注册--AutoServiceRegistration
AutoServiceRegistration
AutoServiceRegistration

AutoServiceRegistration是一个标记接口,所以本身没有实际的意义,仅仅代表了自动注册的意思。

AutoServiceRegistration有个抽象实现AbstractAutoServiceRegistration

AbstractAutoServiceRegistration是个抽象类
AbstractAutoServiceRegistration是个抽象类

AbstractAutoServiceRegistration实现了ApplicationListener,监听了WebServerInitializedEvent事件。

WebServerInitializedEvent这个事件是SpringBoot在项目启动时,当诸如tomcat这类Web服务启动之后就会发布,注意,只有在Web环境才会发布这个事件。

ServletWebServerInitializedEvent继承自WebServerInitializedEvent。

所以一旦当SpringBoot项目启动,tomcat等web服务器启动成功之后,就会触发AbstractAutoServiceRegistration监听器的执行。

最终就会调用ServiceRegistry注册Registration,实现服务自动注册

Nacos自然而然也继承了AbstractAutoServiceRegistration

NacosAutoServiceRegistration
NacosAutoServiceRegistration

对于Nacos而言,就将当前的服务注册的ip和端口等信息,就注册到了Nacos服务注册中心。

所以整个注册流程就可以用这么一张图概括

当然,不仅仅是Nacos是这么实现的,常见的比如Eureka,Zookeeper等注册中心在整合SpringCloud都是实现上面的三板斧。


Ribbon

讲完了SpringCloud环境底下是如何自动注册服务到注册中心的,下面来讲一讲Ribbon。

我们都知道,Ribbon是负载均衡组件,他的作用就是从众多的服务实例中根据一定的算法选择一个服务实例。

但是有个疑问,服务实例的数据都在注册中心,Ribbon是怎么知道的呢???

答案其实很简单,那就是需要注册中心去主动
适配
Ribbon,只要注册中心去适配了Ribbon,那么Ribbon自然而然就知道服务实例的数据了。

Ribbon提供了一个获取服务实例的接口,叫ServerList

ServerList
ServerList

接口中提供了两个方法,这两个方法在众多的实现中实际是一样的,并没有区别。

当Ribbon通过ServerList获取到服务实例数据之后,会基于这些数据来做负载均衡的。

Nacos自然而然也实现了ServerList接口,为Ribbon提供Nacos注册中心中的服务数据。

NacosServerList
NacosServerList

这样,Ribbon就能获取到了Nacos服务注册中心的数据。

同样地,除了Nacos之外,Eureka、Zookeeper等注册中心也都实现了这个接口。

到这,其实就明白了Ribbon是如何知道注册中心的数据了,需要注册中心来适配。

在这里插个个人的看法,其实我觉得Ribbon在适配SpringCloud时对获取服务实例这块支持封装的不太好。

因为SpringCloud本身就是一套约束、规范,只要遵守这套规范,那么就可以实现各个组件的替换,这就是为什么换个注册中心只需要换个依赖,改个配置文件就行。

而Ribbon本身是一个具体的负载均衡组件,注册中心要想整合SpringCloud,还得需要单独去适配Ribbon,有点违背了SpringCloud约束的意义。

就类似mybatis一样,mybatis依靠jdbc,但是mybatis根本不关心哪个数据库实现的jdbc。

真正好的做法是Ribbon去适配SpringCloud时,用SpringCloud提供的api去获取服务实例,这样不同的注册中心只需要适配这个api,无需单独适配Ribbon了。

而SpringCloud实际上是提供了这么一个获取服务实例的api,DiscoveryClient

DiscoveryClient
DiscoveryClient

通过DiscoveryClient就能够获取到服务实例,当然也是需要不同注册中心的适配。

随着Ribbon等组件停止维护之后,SpringCloud官方自己也搞了一个负载均衡组件
loadbalancer
,用来平替Ribbon。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>

这个组件底层在获取服务实例的时候,就是使用的DiscoveryClient。

所以对于
loadbalancer
这个负载均衡组价来说,注册中心只需要实现DiscoveryClient之后就自然而然适配了
loadbalancer


OpenFeign

OpenFeign是一个rpc框架,当我们需要调用远程服务的时候,只需要声明个接口就可以远程调用了,就像下面这样

听上去很神奇,其实本质上就是后面会为接口创建一个动态代理对象,解析类上,方法上的注解。

当调用方法的时候,会根据方法上面的参数拼接一个http请求地址,这个地址的格式是这样的
http://服务名/接口路径

比如,上面的例子,当调用
saveOrder
方法的时候,按照这种规律拼出的地址就是这样的
http://order/order
,第一个order是服务名,第二个order是PostMapping注解上面的。

但是由于只知道需要调用服务的服务名,不知道服务的ip和端口,还是无法调用远程服务,这咋办呢?

这时就轮到Ribbon登场了,因为Ribbon这个大兄弟知道服务实例的数据。

于是乎,OpenFeign就对Ribbon说,兄弟,你不是可以从注册中心获取到order服务所有服务实例数据么,帮我从这些服务实例数据中找一个给我。

于是Ribbon就会从注册中心获取到的服务实例中根据负载均衡策略选择一个服务实例返回给OpenFeign。

OpenFeign拿到了服务实例,此时就获取到了服务所在的ip和端口,接下来就会重新构建请求路径,将路径中的服务名替换成ip和端口,代码如下

reconstructURIWithServer
reconstructURIWithServer
  • Server就是服务实例信息的封装
  • orignal就是原始的url,就是上面提到的, http://order/order

假设获取到的orde服务所在的ip和端口分别是
192.168.2.100

8080
,最终重构后的路径就是
http://192.168.2.100:8080/order
,之后OpenFeign就可以发送http请求了。

至于前面提到的
loadbalancer
,其实也是一样的,他也会根据负载均衡算法,从DiscoveryClient获取到的服务实例中选择一个服务实例给OpenFeign,后面也会根据服务实例重构url,再发送http请求。

loadbalancer组件重构url代码
loadbalancer组件重构url代码


总结

到这,就把Nacos、OpenFeign、Ribbon、loadbalancer等组件协调工作的原理讲完了,其实就是各个组件会预留一些扩展接口,这也是很多开源框架都会干的事,当第三方框架去适配的,只要实现这些接口就可以了。

最后画一张图来总结一下上述组价的工作的原理。

最后再小小地说一句,Nacos、OpenFeign、Ribbon源码剖析的文章,可以从微信公众号
三友的java日记
后台菜单栏中的文章分类中查看。


往期热门文章推荐

RocketMQ重复消费的7种原因

如何去阅读源码,我总结了18条心法

如何实现延迟任务,我总结了11种方法

如何写出漂亮代码,我总结了45个小技巧

三万字盘点Spring/Boot的那些常用扩展点

两万字盘点那些被玩烂了的设计模式

扫码或者搜索关注公众号
三友的java日记
,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

开始

4 月 6 日,根据 Meta AI 官方博客,Meta AI 宣布推出了一个 AI 模型 Segment Anything Model(SAM,分割一切模型)。据介绍,该模型能够根据文本指令等方式实现图像分割,而且万物皆可识别和一键抠图。

github源码地址:
facebookresearch/segment-anything

官方网站体验地址:
segment-anything.com/demo

特点

Segment Anything Model (SAM) 根据输入提示(如点或框)生成高质量的对象蒙版,并可用于为图像中的所有对象生成蒙版。它已经在 11 万张图像和 1 亿个掩码的数据集上进行了训练,并且在各种分割任务上具有强大的零镜头性能。说到底是一种通用的图像分割方法。

初体验

进入官网体验地址后:点击同意条款和条件,大概意思是:

  1. 这是一个研究演示,不得用于任何商业目的
  2. 上传的任何图像将仅用于演示分段任何模型。所有图像和从中派生的任何数据将在会话结束时删除。
  3. 上传的任何图片均不得违反任何知识产权或 Facebook 社群守则。

进入上传界面:

点击“Upload an image”,上传自己的图片:(也可以直接使用官方的图片测试)
image

我上传完图片用时3分11秒,图片大小只有50多K。还有一张4M多的用时几乎差不太多,看来跟图片大小关系不是太大

Hover & Click——选取物体

基本操作:左键选择物体,右键移除选取
image

选取完之后,可以使用Cut out object,或则再点击Multi-mask再次点击选择标记点,可多次分割。

选完之后点击“Cut out object”,然后结果就保存再“Cut-Outs”一栏中。

Box——框选物体

基本操作:按住左键选出一个框框范围
image

保存所选区域的方法仍然是点击“Cut out object”。然后结果就保存再“Cut-Outs”一栏中。

Everything——分割所有物体

不用咱们操作了,直接交给AI处理,提取出所有可分割图形物体
image

所有物体的区域都保存在“Cut-Outs”一栏中:

Cut-Outs——结果提取

基本操作:只需要对Cut-Outs一栏的图片右键点击,并在弹出的菜单中选择“将图片另存为”即可。
image

结语

用起来感觉效果不错,毕竟本人不是做ps或者图片处理的,对图片的质量要求不算高,不过Meta开源了segment-anything框架,属实佩服,毕竟连数据集也可以进行下载,下载地址在github主页下方,目前已经21k+star。希望AI可以继续发展,让「玩具」变成工具
公众号

JavaScript中的垃圾回收机制负责自动管理内存,回收不再使用的对象所占用的内存空间。在JavaScript中,开发者不需要显式地分配和释放内存,垃圾回收器会自动完成这些操作。
以下是关于JavaScript垃圾回收机制的一些关键概念:

  1. 内存生命周期
    :JavaScript内存生命周期包括分配、使用和释放三个阶段。首先,内存会被分配给变量或对象;然后,程序会使用这些变量或对象;最后,不再需要的变量或对象会被垃圾回收器释放。
  2. 可达性
    :垃圾回收器通过可达性来判断一个对象是否还在使用。根对象(如全局对象和其他内置对象)被认为是可达的。如果一个对象可以通过根对象或其他可达对象引用链到达,那么它也被认为是可达的。
  3. 引用计数
    :这是一种较早的垃圾回收策略,通过追踪每个对象的引用次数来判断对象是否仍在使用。当对象的引用计数为0时,表示对象不再被使用,可以被回收。然而,引用计数算法存在循环引用问题,无法回收循环引用的对象。
  4. 标记-清除
    :这是现代JavaScript引擎中常见的垃圾回收算法。标记-清除算法首先会标记所有可达对象,然后遍历整个内存空间,清除未被标记的对象。这种算法可以处理循环引用问题,但可能会导致内存碎片。
  5. 分代回收
    :由于不同对象的生命周期长短不同,现代JavaScript引擎将内存分为新生代和老生代。新生代主要存放短生命周期的对象,老生代主要存放长生命周期的对象。新生代和老生代的垃圾回收策略会有所不同。
  6. 增量回收和懒惰回收
    :为了降低垃圾回收对程序执行的影响,现代JavaScript引擎采用了增量回收和懒惰回收策略。增量回收将回收工作分成多个小任务,穿插在程序执行过程中;懒惰回收则会在一定程度上推迟回收操作,以减少性能开销。

以下是一个简单的示例,演示了 JavaScript 垃圾回收机制中的引用计数和标记清除:

//引用计数示例
let a = { name: 'John'};
let b
= a; //b 引用了 a,a 的引用计数变为 2 a = null; //a 不再引用这个对象,a 的引用计数变为 1 b = null; //b 不再引用这个对象,这个对象的引用计数变为 0,可以被垃圾回收器回收 //标记清除示例 functionfoo() {
let x
= { name: 'Alice'};
let y
= { name: 'Bob'};
x.friend
=y;
y.friend
=x;
}

foo();
//函数执行完后,x 和 y 都不再被使用,但它们之间相互引用,无法使用引用计数来回收内存//垃圾回收器定期运行,会发现 x 和 y 都已经不再被引用,可以被回收

在这个示例中,当变量
a
被赋值给变量
b
时,对象的引用计数变为 2。当
a
被赋值为
null
时,对象的引用计数变为 1。最后当
b
也被赋值为
null
时,对象的引用计数变为 0,可以被垃圾回收器回收。

另外,函数
foo
中创建了两个对象
x

y
,并且它们相互引用。在函数执行完后,这两个对象不再被使用,但它们之间的引用关系无法使用引用计数来回收内存。因此,垃圾回收器会定期运行,查找那些已经不再被引用的对象,然后释放它们所占用的内存空间。


再来一个例子,我们将创建一些对象并解释JavaScript的垃圾回收机制。

//创建对象
functioncreatePerson(name, age) {return{
name: name,
age: age,
};
}
//创建两个对象 let person1 = createPerson("Alice", 30);
let person2
= createPerson("Bob", 35);//person1 和 person2 变量引用了两个新创建的对象,这些对象在内存中是可达的 //现在将 person1 引用另一个对象 person1 = createPerson("Charlie", 28);//之前 person1 引用的 "Alice" 对象现在已经不再可达,因为没有变量引用它//JavaScript的垃圾回收器会识别到这一点,并在合适的时机释放其内存 //创建一个循环引用 let objA ={
name:
"ObjA",
};
let objB
={
name:
"ObjB",
};
objA.link
=objB;
objB.link
=objA;//将变量设置为 null,打破可达性 objA = null;
objB
= null;//现在 objA 和 objB 对象都不再可达,即使它们彼此引用//使用标记-清除算法的垃圾回收器会识别到这一点,并释放它们占用的内存

在这个例子中,我们创建了几个对象并对它们进行了引用。当一个对象不再可达时,它就成为了垃圾回收的目标。对于循环引用的情况,标记-清除算法可以识别到并正确处理这种情况,释放不再使用的对象所占用的内存。

注意:不同的JavaScript引擎可能采用不同的垃圾回收策略,如
V8

SpiderMonkey

JavaScriptCore

等。

本系列文章导航
  1. https://www.cnblogs.com/aierong/p/17300066.html
  2. https://github.com/aierong/WpfDemo (自我Demo地址)


希望提到的知识对您有所提示,同时欢迎交流和指正
作者:
aierong
出处:
https://www.cnblogs.com/aierong

说明

CommunityToolkit.Mvvm8.1最令人惊喜的是它提供的源生成器功能,它极大简化我们的mvvm代码
我们通过标记一个属性就可以实现某个功能,这个很方便快捷,推荐

常用标记总结
1.继承ObservableObject 并且类标记是分部类partial
2.私有变量标记属性 [ObservableProperty]
3.NotifyCanExecuteChangedFor  通知依赖命令
NotifyPropertyChangedFor    通知依赖属性
4.RelayCommand  定义命令
5.OnPropertyChanged 手动通知属性更新
6.ButtonClickCommand.NotifyCanExecuteChanged() 手动通知命令更新
7.OnLastNameChanging OnLastNameChanged  某个属性改变
8.OnPropertyChanged  所有属性改变

定义viewmodel

定义vm时,请使用分部类,并且继承ObservableObject

public partial class DataViewModel2 : ObservableObject
{

}

ObservableProperty标记属性

定义属性如此简单:一个[ObservableProperty]标记搞定

/*
[ObservableProperty]标记后,会自动生成属性(大写命名),例如:下面会自动生成Title

注意:这个私有变量命名:必须是小写开头,或者下划线,或者m_
*/

[ObservableProperty]
private string title = "hello";

//public string Title
//{
//    get
//    {
//        return title;
//    }
//    set
//    {
//        //title = value;
//        //PropertyChanged?.Invoke( this , new PropertyChangedEventArgs( "Name" ) );

//        //SetProperty 相当与设置值,并且PropertyChanged通知调用
//        SetProperty( ref title , value );
//    }
//}

NotifyPropertyChangedFor通知依赖属性

[NotifyPropertyChangedFor( nameof( Caption ) )]标识:在LastName改变后,去通知Caption

public string Caption
{
    get
    {
        return string.Format( "Title:{0}-{1}" , Title , LastName );
    }
}


[ObservableProperty]
[NotifyPropertyChangedFor( nameof( Caption ) )]
private string lastName = "abc";

NotifyCanExecuteChangedFor通知依赖命令

在属性IsEnabled改变后,通知命令:ButtonClickCommand

/*
        [NotifyCanExecuteChangedFor( nameof( ButtonClickCommand ) )]
NotifyCanExecuteChangedFor是通知依赖命令(触发命令),相当于set中ButtonClickCommand.NotifyCanExecuteChanged();
*/

[ObservableProperty]
[NotifyCanExecuteChangedFor( nameof( ButtonClickCommand ) )]
private bool isEnabled = false;

//public bool IsEnabled
//{
//    get => isEnabled;
//    set
//    {
//        SetProperty( ref isEnabled , value );

//        //通知命令 已经改变
//        ButtonClickCommand.NotifyCanExecuteChanged();
//    }
//}

//partial void OnIsEnabledChanged ( bool value )
//{
//     //如果上面的[NotifyCanExecuteChangedFor( nameof( ButtonClickCommand ) )]不写,可以这里手动通知更新 
//    //ButtonClickCommand.NotifyCanExecuteChanged();
//}

命令

RelayCommand标识定义一个命令,如此简单

/*
RelayCommand是定义命令,自动生成的命令名是方法名+Command,并且初始化
例如:下面的会自动生成ButtonClickCommand

CanExecute是指定一个判断方法,判断是否可用
*/

[RelayCommand( CanExecute = nameof( CanButton ) )]
void ButtonClick ()
{
    //点击按钮,修改标题
    Title = "hello(改)";
}

bool CanButton ()
{
    return IsEnabled;
}

//public RelayCommand ButtonClickCommand
//{
//    get;
//}



[RelayCommand]
void ButtonClickPar ( double val )
{
    Title = $"hello(改):{val}";
}

//public RelayCommand<double> ButtonClickParCommand
//{
//    get;
//}

异步命令

把方法标识为async,即可定义为异步命令,它带有一个IsRunning属性,可以在view中做进度条判断

[RelayCommand]
async Task AsyncButtonClick ()
{
    await Task.Delay( 4800 );
    Title = "hello(Task改)";
}



[RelayCommand]
async Task AsyncButtonParClick ( double val )
{
    await Task.Delay( 4800 );
    Title = $"hello(Task改):{val}";
}
<!--   
特别说明:异步命令会自动控制控件的可见性,并且提供一个IsRunning属性可以判断异步是否完成   
-->
<Button Width="100"
        Height="30"
        Command="{Binding AsyncButtonClickCommand}"
        Content="异步" />
<TextBlock HorizontalAlignment="Center"
           FontSize="20"
           FontStyle="Italic"
           FontWeight="Bold"
           Foreground="Green"
           Text="loading......"
           Visibility="{Binding AsyncButtonClickCommand.IsRunning, Converter={StaticResource myboolconvert}}" />

某个属性改变

On+属性Changing  On+属性Changed,可以记录某个属性值变化事件

/*
还可以实现2个方法:OnLastNameChanging OnLastNameChanged (注意2个方法只可以实现其中一个,或者都不实现(不能同时2个))
*/

//partial void OnLastNameChanging ( string value )
//{
//    Debug.WriteLine( value );
//}

partial void OnLastNameChanged ( string value )
{
    // 可以做一些其它事情 例如:属性改变后,消息通知某某某
    Debug.WriteLine( value );



    //说明:如果上面[NotifyPropertyChangedFor( nameof( Caption ) )]不写,可以这里手动通知属性更新
    //OnPropertyChanged( nameof( Caption ) );
}

所有属性改变

所有属性改变后都会调用这个事件,参数PropertyName可以区分具体哪个属性

/// <summary>
/// 所有属性改变
/// </summary>
/// <param name="e"></param>
protected override void OnPropertyChanged ( PropertyChangedEventArgs e )
{

    base.OnPropertyChanged( e );

    // 可以获取到是哪个属性改变了
    var _proname = e.PropertyName;
}

完整代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

/*
这里演示自动生成属性和命令

1.继承ObservableObject 并且类标记是分部类partial
2.私有变量标记属性 [ObservableProperty]
3.NotifyCanExecuteChangedFor  通知依赖命令
  NotifyPropertyChangedFor    通知依赖属性
4.RelayCommand  定义命令
5.OnPropertyChanged 手动通知属性更新
6.ButtonClickCommand.NotifyCanExecuteChanged() 手动通知命令更新
7.OnLastNameChanging OnLastNameChanged  某个属性改变
8.OnPropertyChanged  所有属性改变
*/

namespace WpfDemoNet6.Demo
{
    public partial class DataViewModel2 : ObservableObject
    {
        /*
        [ObservableProperty]标记后,会自动生成属性(大写命名),例如:下面会自动生成Title

        注意:这个私有变量命名:必须是小写开头,或者下划线,或者m_
        */

        /*
        NotifyPropertyChangedFor 通知依赖属性Caption
        */

        [ObservableProperty]
        [NotifyPropertyChangedFor( nameof( Caption ) )]
        private string title = "hello";

        //public string Title
        //{
        //    get
        //    {
        //        return title;
        //    }
        //    set
        //    {
        //        //title = value;
        //        //PropertyChanged?.Invoke( this , new PropertyChangedEventArgs( "Name" ) );

        //        //SetProperty 相当与设置值,并且PropertyChanged通知调用
        //        SetProperty( ref title , value );
        //    }
        //}


        /*
                [NotifyCanExecuteChangedFor( nameof( ButtonClickCommand ) )]
        NotifyCanExecuteChangedFor是通知依赖命令(触发命令),相当于set中ButtonClickCommand.NotifyCanExecuteChanged();
        */

        [ObservableProperty]
        [NotifyCanExecuteChangedFor( nameof( ButtonClickCommand ) )]
        private bool isEnabled = false;

        //public bool IsEnabled
        //{
        //    get => isEnabled;
        //    set
        //    {
        //        SetProperty( ref isEnabled , value );

        //        //通知命令 已经改变
        //        ButtonClickCommand.NotifyCanExecuteChanged();
        //    }
        //}

        //partial void OnIsEnabledChanged ( bool value )
        //{
        //     //如果上面的[NotifyCanExecuteChangedFor( nameof( ButtonClickCommand ) )]不写,可以这里手动通知更新 
        //    //ButtonClickCommand.NotifyCanExecuteChanged();
        //}




        /*
        RelayCommand是定义命令,自动生成的命令名是方法名+Command,并且初始化
        例如:下面的会自动生成ButtonClickCommand

        CanExecute是指定一个判断方法,判断是否可用
        */

        [RelayCommand( CanExecute = nameof( CanButton ) )]
        void ButtonClick ()
        {
            //点击按钮,修改标题
            Title = "hello(改)";
        }

        bool CanButton ()
        {
            return IsEnabled;
        }

        //public RelayCommand ButtonClickCommand
        //{
        //    get;
        //}



        public DataViewModel2 ()
        {
            //RelayCommand的第一个参数是命令调用语句
            //              第2个参数(可选)是否允许使用
            //ButtonClickCommand = new RelayCommand( () =>
            //{
            //    //点击按钮,修改标题
            //    Title = "hello(改)";
            //} , () =>
            //{
            //    return IsEnabled;
            //} );

            //ButtonClickParCommand = new RelayCommand<double>( ( double val ) =>
            //{
            //    Title = $"hello(改):{val}";
            //} );
        }



        [RelayCommand]
        void ButtonClickPar ( double val )
        {
            Title = $"hello(改):{val}";
        }

        //public RelayCommand<double> ButtonClickParCommand
        //{
        //    get;
        //}



        public string Caption
        {
            get
            {
                return string.Format( "Title:{0}-{1}" , Title , LastName );
            }
        }


        [ObservableProperty]
        [NotifyPropertyChangedFor( nameof( Caption ) )]
        private string lastName = "abc";

        /*
        还可以实现2个方法:OnLastNameChanging OnLastNameChanged (注意2个方法只可以实现其中一个,或者都不实现(不能同时2个))
        */

        //partial void OnLastNameChanging ( string value )
        //{
        //    Debug.WriteLine( value );
        //}

        partial void OnLastNameChanged ( string value )
        {
            // 可以做一些其它事情 例如:属性改变后,消息通知某某某
            Debug.WriteLine( value );



            //说明:如果上面[NotifyPropertyChangedFor( nameof( Caption ) )]不写,可以这里手动通知属性更新
            //OnPropertyChanged( nameof( Caption ) );
        }



        /// <summary>
        /// 所有属性改变
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPropertyChanged ( PropertyChangedEventArgs e )
        {

            base.OnPropertyChanged( e );

            // 可以获取到是哪个属性改变了
            var _proname = e.PropertyName;
        }

    }
}

导航

https://github.com/aierong/WpfDemo/tree/main/WpfDemoNet6 (项目地址)

https://github.com/aierong/WpfDemo/blob/main/WpfDemoNet6/Demo/DataViewModel2.cs (代码地址)