当前位置:网站首页 / wpf in net6 / 正文

ay wpf in net6 第4天 - 引入CommunityToolkitMvvm 8.0 消息接收

时间:2022年05月15日 | 作者 : aaronyang | 分类 : wpf in net6 | 浏览: 1805次 | 评论 0

属性通知约定

增加了OnXXXChanging方法和OnXXXChanged


修改代码如下

         [ObservableProperty]
        [AlsoNotifyCanExecuteFor(nameof(ShowInformationCommand), nameof(Show2Command))]
        [AlsoNotifyChangeFor(nameof(Info),nameof(Info2))]
        private string stuName;

        [ObservableProperty]
        private string stuClass;

        [ObservableProperty]
        private string output;

        public string Info => $"你输入的姓名:{StuName}";

        public string Info2 { get; set; }

        partial void OnStuNameChanging(string value)
        {
            Info2 += "姓名将要变为" + value+"\r\n";
        }
        partial void OnStuNameChanged(string value)
        {
            Info2 += "姓名已经变为" + value + "\r\n";
        }

输入partial就已经开始提示了

image.png


xaml

<Window x:Class="AyWpfNet6.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AyWpfNet6"
        mc:Ignorable="d" WindowStartupLocation="CenterScreen"
        Title="www.ayjs.net" Height="450" Width="800"
        DataContext="{Binding Source={StaticResource Locator},Path=MainViewModel}"
        >
    <Grid Margin="10">

        <StackPanel Width="400">
            <TextBlock Text="姓名"/>
            <TextBox Text="{Binding StuName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
            <TextBlock Text="班级"/>
            <TextBox Text="{Binding StuClass,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>

            <TextBlock Text="{Binding Info}" Margin="0,5"/>
            
            <Button Content="显示信息" Command="{Binding ShowInformationCommand}"/>
            <Button Content="显示2" Command="{Binding Show2Command}"/>
            <Button Content="显示3" Command="{Binding Show3Command}"/>
            
            <Label Content="{Binding Output}" FontSize="14"/>

            <TextBlock Text="{Binding Info2}" Margin="0,5" TextWrapping="Wrap"/>
        </StackPanel>
    </Grid>
</Window>


image.png


参考 Introduction to the MVVM Toolkit - Windows Community Toolkit | Microsoft Docs

image.png

这个文档是7.X的,作为基础,我也是边学边记笔记


DEMO1 包含一个非通知对象

官方例子

public class ObservableUser : ObservableObject
{
    private readonly User user;

    public ObservableUser(User user) => this.user = user;

    public string Name
    {
        get => user.Name;
        set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
    }
}

我们添加Classroom类

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AyWpfNet6.ViewModels
{
    public class Classroom
    {
        public string ClassName { get; set; }
    }
    public partial class MainWindowViewModel : ObservableObject
    {
        private readonly Classroom classroom;
        //private string stuName;

        //public string StuName
        //{
        //    get { return stuName; }
        //    set
        //    {
        //        if (SetProperty(ref stuName, value))
        //        {
        //            ShowInformation.NotifyCanExecuteChanged();
        //        }
        //    }
        //}
        [ObservableProperty]
        [AlsoNotifyCanExecuteFor(nameof(ShowInformationCommand), nameof(Show2Command))]
        [AlsoNotifyChangeFor(nameof(Info), nameof(Info2))]
        private string stuName;

        [ObservableProperty]
        private string stuClass;

        [ObservableProperty]
        private string output;

        public string Info => $"你输入的姓名:{StuName}";

        public string Info2 { get; set; }

        partial void OnStuNameChanging(string value)
        {
            Info2 += "姓名将要变为" + value + "\r\n";
        }
        partial void OnStuNameChanged(string value)
        {
            Info2 += "姓名已经变为" + value + "\r\n";
        }


        //private string stuClass;

        //public string StuClass
        //{
        //    get { return stuClass; }
        //    set
        //    {
        //        SetProperty(ref stuClass, value);
        //    }
        //}
        //private string output;

        //public string Output
        //{
        //    get { return output; }
        //    set
        //    {
        //        SetProperty(ref output, value);
        //    }
        //}
        //public RelayCommand ShowInformation { get; }


        //public MainWindowViewModel(ILogger<MainWindowViewModel> logger)
        //{
        //    ShowInformation = new RelayCommand(async() => await DoShowInformation());
        //}

        public AsyncRelayCommand ShowInformationCommand { get; }



        public MainWindowViewModel(ILogger<MainWindowViewModel> logger, Classroom classroom)
        {
            ShowInformationCommand = new AsyncRelayCommand(DoShowInformation, () => !string.IsNullOrWhiteSpace(StuName));
            this.classroom = classroom;
            //ShowInformation = new AsyncRelayCommand(DoShowInformation);
        }

        public string ClassName
        {
            get { return classroom.ClassName; }
            set { SetProperty(classroom.ClassName, value, classroom, (cl, n) => cl.ClassName = n); }
        }


        private bool CanShow2()
        {
            return !string.IsNullOrWhiteSpace(StuName);
        }


        [ICommand(CanExecute = nameof(CanShow2))]
        private void Show2()
        {

            //await Task.Delay(2000);
            Output = $"我是show2,你好,{StuName} 班级 {StuClass}";
        }

        int clickcount = 0;
        [ICommand(AllowConcurrentExecutions = false)]
        private async Task Show3()
        {
            //Show3Command
            clickcount++;
            await Task.Delay(2000);
            Output = $"我是show3,你好,{StuName} 班级 {StuClass},被单击了{clickcount}次";
        }



        private async Task DoShowInformation()
        {
            //StuName += string.Empty;
            //StuClass += string.Empty;
            //Output += string.Empty;
            await Task.Delay(2000);
            Output = $"你好,{StuName} 班级 {StuClass}";
            StuName = "";
            StuClass = "";
        }
    }
}

然后App.xaml.cs

   private void ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services)
        {
            services.AddSingleton<IStudentService, StudentService>();
            services.AddSingleton<MainWindow>();
            services.AddSingleton<MainWindowViewModel>();
            services.AddTransient<Classroom>();
            services.AddTransient<TwoWindow>();
        }

xaml

<Window x:Class="AyWpfNet6.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AyWpfNet6"
        mc:Ignorable="d" WindowStartupLocation="CenterScreen"
        Title="www.ayjs.net" Height="450" Width="800"
        DataContext="{Binding Source={StaticResource Locator},Path=MainViewModel}"
        >
    <Grid Margin="10">

        <StackPanel Width="400">
            <TextBlock Text="姓名"/>
            <TextBox Text="{Binding StuName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
            <TextBlock Text="班级"/>
            <TextBox Text="{Binding StuClass,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>

            <TextBlock Text="班级2"/>
            <TextBox Text="{Binding ClassName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>

            <TextBlock Text="{Binding Info}" Margin="0,5"/>
            <TextBlock Text="{Binding ClassName}" Margin="0,5"/>
            <Button Content="显示信息" Command="{Binding ShowInformationCommand}"/>
            <Button Content="显示2" Command="{Binding Show2Command}"/>
            <Button Content="显示3" Command="{Binding Show3Command}"/>
            
            <Label Content="{Binding Output}" FontSize="14"/>

            <TextBlock Text="{Binding Info2}" Margin="0,5" TextWrapping="Wrap"/>
        </StackPanel>
    </Grid>
</Window>

如果这样做适配器方式的完成,我觉得不如新建个类,不过要维护2个类,还要赋值和设置值,这里二次wrap了一下。

测试:输入班级2,下方绑定这个属性的也通知了

image.png


DEMO2

如果一个属性是一个任务,那么一旦任务完成,还需要引发通知事件,以便在正确的时间更新绑定。 例如。 显示任务所代表的操作的加载指示器或其他状态信息。 ObservableObject 有一个用于这种场景的 API:

我们VM添加一个带通知的Task的属性

vm

  private TaskNotifier<int>? requestTask;

        public Task<int>? RequestTask
        {
            get => requestTask;
            set => SetPropertyAndNotifyOnCompletion(ref requestTask, value);
        }
        [ICommand]
        public async void RequestValue()
        {
            await Task.Delay(2000);
            RequestTask = Task.FromResult(100);
        }


xaml

    <Button Content="调用RequestValue" Command="{Binding RequestValueCommand}"/>
            <TextBlock Text="显示Task通知属性"/>
            <TextBox Text="{Binding RequestTask.Result,Mode=OneWay}"/>


效果,单击按钮,执行任务,返回值,前台就会显示了,注意是单向绑定。

感觉这样的好处,省去了一个带通知的属性声明



IMessenger

MVVM Toolkit 提供了两种开箱即用的实现:WeakReferenceMessenger 和 StrongReferenceMessenger:前者在内部使用弱引用,为收件人提供自动内存管理,而后者使用强引用并要求开发人员在不再需要时手动取消订阅他们的收件人

ObservableRecipient 

它是可观察对象的基类,它也充当消息的接收者。此类是 ObservableObject 的扩展,它还提供内置支持以使用 IMessenger 类型。

接收消息的类,IsActive属性激活和停用VM,改变触发事件 OnActivated和OnDeactivated事件,公开Messenger属性和Broadcast<T>(T,T,string).


vm支持消息的写法

1 可以直接给VM实现IRecipient<T>接口

2 OnActivated时候注册接收消息


DEMO3

C#

        [ICommand]
        private void ShowTwoWindow()
        {
            var window = App.ServiceProvider.GetRequiredService<TwoWindow>();
            window.Show();
        }

xaml

            <Button Content="显示TwoWindow" Command="{Binding ShowTwoWindowCommand}"/>

现在单击按钮可以显示显示TwoWindow窗体


====================www.ayjs.net       杨洋    wpfui.com        ayui      ay  aaronyang=======请不要转载谢谢了。=========


场景:MainWindowViewModel接收TwoWindow发来的消息,显示code

接下来修改MainWindow作为消息接收方,注册接收消息和接收后的处理

    public class MessageBase
    {
        public string Code { get; set; }
        public string Value { get; set; }
    }

    public class ShowLoginMessage : MessageBase
    {
        public string LoginName { get; set; }
    }


    public partial class MainWindowViewModel : ObservableRecipient,IRecipient<MessageBase>
    {
        public void Receive(MessageBase message)
        {
            MessageCode = message.Code;
        }
        [ObservableProperty]
        private string messageCode;
        [ICommand]
        private void Show4()
        {
            base.Messenger.Send(new MessageBase { Code = "AY 合肥 1991", Value = "来自this" });
        }

xaml

<Window x:Class="AyWpfNet6.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AyWpfNet6"
        mc:Ignorable="d" WindowStartupLocation="CenterScreen"
        Title="www.ayjs.net" Height="450" Width="800"
        DataContext="{Binding Source={StaticResource Locator},Path=MainViewModel}"
        >
    <Grid Margin="10">

        <StackPanel Width="400">
            <TextBlock Text="姓名"/>
            <TextBox Text="{Binding StuName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
            <TextBlock Text="班级"/>
            <TextBox Text="{Binding StuClass,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>

            <TextBlock Text="班级2"/>
            <TextBox Text="{Binding ClassName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>

            <TextBlock Text="{Binding Info}" Margin="0,5"/>
            <TextBlock Text="{Binding ClassName}" Margin="0,5"/>
            <Button Content="显示信息" Command="{Binding ShowInformationCommand}"/>
            <Button Content="显示2" Command="{Binding Show2Command}"/>
            <Button Content="显示3" Command="{Binding Show3Command}"/>
            <Label Content="{Binding Output}" FontSize="14"/>
            <Button Content="调用RequestValue" Command="{Binding RequestValueCommand}"/>
            <TextBlock Text="显示Task通知属性"/>
            <TextBox Text="{Binding RequestTask.Result,Mode=OneWay}"/>
            <TextBlock Text="{Binding Info2}" Margin="0,5" TextWrapping="Wrap"/>
            <Button Content="显示4" Command="{Binding Show4Command}"/>
            <Button Content="显示TwoWindow" Command="{Binding ShowTwoWindowCommand}"/>
        </StackPanel>
        <Grid x:Name="loginArea" Width="100" Height="100" Background="Yellow" HorizontalAlignment="Right" VerticalAlignment="Top">
            <TextBlock Text="{Binding MessageCode}" Margin="0,5" TextWrapping="Wrap"/>
        </Grid>
    </Grid>
</Window>

通过实现IRecipient<T>这个接口,vm自动注册Register<this,消息

继承 ObservableRecipient后,自动有个Messenger属性,直接注册和发送消息

然后你需要手动设置this.IsActive = true;我在构造函数设置的,不然vm收不到消息的,设置false,就取消收消息了。

image.png

添加Show4方法,前台增加个Show4Command调用的Button

        [ICommand]
        private void Show4()
        {
            Messenger.Send(new MessageBase { Code = "AY 合肥 1991", Value = "来自this" });
        }

image.png

你说这样,我同一个vm不如直接调用Show4方法的,那也对的,那么MainWindow.xaml.cs调用vm这个方法呢??怎么调用,其他窗体怎么调用?

比如学生列表,单击弹出新增窗口,点击保存个学生,然后调用学生列表窗口刷新,其实就可以发送消息,刷新学生列表。


你也可以使用WeakReferenceMessenger和StrongReferenceMessenger来注册和发送消息

添加个Show5的按钮,xaml.cs发送消息

        private void Show5_Click(object sender, RoutedEventArgs e)
        {
            WeakReferenceMessenger.Default.Send(new MessageBase { Code = "AY 合肥 1991,来自mainwindowxaml", Value = "来自this" });
        }

image.png

添加TwoWindowViewModel,像MainWindowViewModel发送消息

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AyWpfNet6.ViewModels
{
    public  class ViewModelLocator
    {
        public MainWindowViewModel MainViewModel => App.ServiceProvider.GetRequiredService<MainWindowViewModel>();

        public TwoWindowViewModel TwoWindowViewModel => App.ServiceProvider.GetRequiredService<TwoWindowViewModel>();
    }
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AyWpfNet6.ViewModels
{
  

    public partial class TwoWindowViewModel : ObservableRecipient
    {
  
        [ObservableProperty]
        private string inputCode;

     
        public TwoWindowViewModel()
        {
         
        }

      
    

        [ICommand]
        public  void ShowCode()
        {
            WeakReferenceMessenger.Default.Send<MessageBase>(new MessageBase { Code = "AY1991来自TwoWindowViewModel", Value = "来自twowindow" });
        }

      

  
    }
}
<Window x:Class="AyWpfNet6.TwoWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AyWpfNet6"
        mc:Ignorable="d"      DataContext="{Binding Source={StaticResource Locator},Path=TwoWindowViewModel}"
        Title="TwoWindow" Height="450" Width="800">
    <Grid>
        <StackPanel>
            <TextBox HorizontalAlignment="Left" Margin="275,155,0,0" TextWrapping="Wrap" Text="{Binding InputCode}" VerticalAlignment="Top" Width="200"/>
            <Button Content="调用MainWindow的显示Code" Command="{Binding ShowCodeCommand}"/>
        </StackPanel>
    </Grid>
</Window>

App.xaml.cs

        private void ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services)
        {

            services.AddSingleton<IStudentService, StudentService>();
            services.AddSingleton<MainWindow>();
            services.AddSingleton<MainWindowViewModel>();

            services.AddTransient<Classroom>();

            services.AddTransient<TwoWindow>();
            services.AddTransient<TwoWindowViewModel>();
        }

image.png

单击后MainWindowViewModel收到消息并且处理了。


接下来我们演示非实现接口的方式注册消息,这也是推荐的方式

twowindowviewmodel接收 mainwindowviewmodel的消息

单击显示5按钮发送消息给twowindow

MainWindow.xaml.cs

       private void Show5_Click(object sender, RoutedEventArgs e)
        {
            WeakReferenceMessenger.Default.Send(new MessageBase { Code = "AY 合肥 1991,来自mainwindowxaml", Value = "来自this" });
            WeakReferenceMessenger.Default.Send(new ShowLoginMessage { Code = "AY来自MainWindow", Value = "来自this",LoginName="杨洋" });
            
        }

TwoWindowViewModel.cs

    public partial class TwoWindowViewModel : ObservableRecipient
    {
        protected override void OnActivated()
        {
            WeakReferenceMessenger.Default.Register<ShowLoginMessage>(this, (r, m) =>
            {
                //收到消息怎么处理
                InputCode = m.LoginName;
            });
        }
        protected override void OnDeactivated()
        {
            base.OnDeactivated();
            WeakReferenceMessenger.Default.Unregister<ShowLoginMessage>(this);
        }

image.png

如图,消息已经发送到twowindow去了。


测试2,弹出3个TwoWindow,只发一个消息,看看2个会不会重复接收

单击一次,3个窗体都收到了消息

image.png

注意前台文本框Text绑定方式

   <TextBox HorizontalAlignment="Left" Margin="275,155,0,0" TextWrapping="Wrap" Text="{Binding InputCode,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="200"/>

不然手动修改文本框后,点击显示5,文本不会变,如上写就会变。


我发现我的OnDeactivated没有用调用,应该会自动取消吧

要手动设置     IsActive = true;不然不接收消息

       public TwoWindowViewModel()
        {
            IsActive = true;
        }


接下来按照token注册,注册消息时候给个名字,在任意地方send这个名字,就可以正确调用

image.png

打开TwoWindowViewModel,注册多个消息,

           protected override void OnActivated()
        {
            WeakReferenceMessenger.Default.Register<ShowLoginMessage>(this, (r, m) =>
            {
                InputCode = m.LoginName;
            });
            WeakReferenceMessenger.Default.Register<ShowLoginMessage,string>(this,"TwoWindowViewModel_show1", (r, m) =>
            {
                InputCode ="当前登陆:"+ m.LoginName;
            });
            WeakReferenceMessenger.Default.Register<ShowLoginMessage,string>(this,"TwoWindowViewModel_show2", (r, m) =>
            {
                InputCode = "当前2登陆:" + m.LoginName;
            });
        }

然后在MainWindow.xaml.cs测试调用

   private void Show5_Click(object sender, RoutedEventArgs e)
        {
            WeakReferenceMessenger.Default.Send(new MessageBase { Code = "AY 合肥 1991,来自mainwindowxaml", Value = "来自this" });
            WeakReferenceMessenger.Default.Send(new ShowLoginMessage { Code = "AY来自MainWindow", Value = "来自this",LoginName="杨洋" });

        }
        private void Show6_Click(object sender, RoutedEventArgs e)
        {
            WeakReferenceMessenger.Default.Send(new ShowLoginMessage { Code = "AY来自MainWindow", Value = "来自this", LoginName = "杨洋" }, "TwoWindowViewModel_show1" );

        }

效果:单击show5按钮,显示杨洋,单击show6,自动找到第1个 注册的消息处理,显示当前登陆:杨洋


讲解:消息注册:

第一个注册,是把当前的类.GetType()当做 标识,只要发送的消息标识是这个类型的,这里都会收到。

第二个注册,泛型<ShowLoginMessage,string>  第1个泛型参数是 方法的参数类型,第二个参数是 标识类型,或者说token类型,一般用string,后面第一个参数this,在当前类型,叫token名字的,参数是ShowLoginMessage类型的,符合这3个条件的,都会到对应的 消息handler中处理。

第三个同第二个,只是消息token不一样。

我们日常使用,都是用token,因为这样精准定位消息,确定随着消息注册的多,不好维护,所以token的名字最好有个地方维护,最好强类型的,这样send和register就可以同一个名字了。

// Unregisters the recipient from a message type
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage>(this);

// Unregisters the recipient from a message type in a specified channel
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage, int>(this, 42);

// Unregister the recipient from all messages, across all channels
WeakReferenceMessenger.Default.UnregisterAll(this);

如上取消注册消息

第一个 取消当前type的下注册的 参数接收是LoggedInUserChangedMessage 的注册

第二个 取消当前type的下注册的 参数接收是LoggedInUserChangedMessage,token是int类型叫42的(我们一般用string,他用的int类型)  的注册

第三个 取消当前type的下注册的不考虑任何类型参数,任何toke的注册

如果你担心内存啥的,你也可以

this.Unloaded += (sender, e) => WeakReferenceMessenger.Default.UnregisterAll(this);


同理注册也是

// Register that specific message...
WeakReferenceMessenger.Default.Register<LoggedInUserChangedMessage>(this);

// ...or alternatively, register all declared handlers
WeakReferenceMessenger.Default.RegisterAll(this);

// Send a message from some other module
WeakReferenceMessenger.Default.Send(new LoggedInUserChangedMessage(user));


部分参考文档:Messenger - Windows Community Toolkit | Microsoft Docs





关于ObservableValidator,RequestMessage<T> 还有个Ioc,还有个高性能  知识,下篇再讲

====================www.ayjs.net       杨洋    wpfui.com        ayui      ay  aaronyang=======请不要转载谢谢了。=========




推荐您阅读更多有关于“wpf in net6,”的文章

猜你喜欢

额 本文暂时没人评论 来添加一个吧

发表评论

必填

选填

选填

必填

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

  查看权限

抖音:wpfui 工作wpf,兴趣学习flutter

目前在合肥市某公司上班,已经厌弃,如果你的公司看的上我,加我QQ私聊

AYUI8全源码 Github地址:前往获取

杨洋(AaronYang简称AY,安徽六安人)AY唯一QQ:875556003和AY交流

高中学历,2010年开始web开发,2015年1月17日开始学习WPF

声明:AYUI7个人与商用免费,源码可购买。部分DEMO不免费

不是从我处购买的ayui7源码,我不提供任何技术服务,如果你举报从哪里买的,我可以帮你转正为我的客户,并送demo

查看捐赠

AYUI7.X MVC教程 更新如下:

第一课 第二课 程序加密教程

标签列表