基于Avalonia 11.0.0+ReactiveUI 的跨平台项目开发2-功能开发
基于Avalonia 11.0.0+ReactiveUI 的跨平台项目开发2-功能开发
项目简介:目标是开发一个跨平台的AI聊天和其他功能的客户端平台。目的来学习和了解Avalonia。将这个项目部署在openKylin 1.0 的系统上。
为什么使用Avalonia:之前已经了解了基于Avalonia的项目在国产麒麟系统中运行的案例。正是Avalonia在跨平台的出色表现,学习和了解Avalonia这个UI框架显得十分有必要。本项目采用的是最新稳定版本11.0.0-rc1.1。希望通过该项目了解和学习Avalonia开发的朋友可以在我的github上拉取代码,同时希望大家多多点点star。
https://github.com/raokun/TerraMours.Chat.Ava
项目的基础框架和通用功能在上一篇博客中介绍过了,想了解的同学跳转学习:
基于Avalonia 11.0.0+ReactiveUI 的跨平台项目开发1-通用框架
了解Avalonia创建模板项目-基础可跳转:
本次我主要分享的内容是项目中具体功能开发实现的过程和各技术的应用
1.功能介绍
1.界面交互
第一版的内容主要分为以下几个模块:
- LoadView.axaml 加载界面:系统打开时候的加载界面,用于首页替换的技术实践。可改造成登陆界面。
- MainWindow.axaml 首页
- MainView.axaml 主界面
- DataGridView.axaml 会话列表
- ChatView.axaml 聊天界面
- ApiSettingsView.axaml API配置
2.功能实现
下面我会按照各个模块来介绍对应的功能和实现方法。
1.加载界面
加载界面 是系统的首个加载界面,界面样式如下:
1.作用和功能:
加载界面是系统在运行前的准备界面,目前并没有做什么操作,只是做了个进度条,到100%时跳转首页。不过这是一个可扩展的实践。
加载界面完成了首页的切换的实践,为后期登录页面做好了准备。同时,加载界面的内容,改写成蒙版,在需要长时间数据处理用于限制用户操作也是不错的选择。
2.设置加载界面为项目运行时首个加载界面
设置首个加载界面,需要在App.axaml.cs中的OnFrameworkInitializationCompleted方法中设置 desktop.MainWindow
OnFrameworkInitializationCompleted代码如下:
public override void OnFrameworkInitializationCompleted() {
//添加共享资源
var VMLocator = new VMLocator();
Resources.Add("VMLocator", VMLocator);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
var load= new LoadView {
DataContext = new LoadViewModel(),
};
desktop.MainWindow = load;
VMLocator.LoadViewModel.ToMainAction = () =>
{
desktop.MainWindow = new MainWindow();
desktop.MainWindow.Show();
load.Close();
};
}
base.OnFrameworkInitializationCompleted();
}
3.隐藏窗口的关闭按钮,并设置窗口居中显示
加载界面不应该有关闭等按钮,我们用 SystemDecorations="None"。将 SystemDecorations
属性设置为 "None"
可以隐藏窗口的系统装饰。系统装饰包括标题栏、最小化、最大化和关闭按钮等。通过设置 SystemDecorations
为 "None"
,可以使窗口更加定制化和个性化,同时减少了不必要的系统装饰。
界面应该显示在屏幕正中间。我们用 WindowStartupLocation="CenterScreen"。设置 WindowStartupLocation
为 "CenterScreen"
可以使窗口在屏幕上居中显示。
4.实现进度条
代码如下:
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical"
Spacing="10">
<TextBlock
Text="Loading..."
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"
Background="Transparent"/>
<ProgressBar
x:Name="progressBar"
HorizontalAlignment="Center"
Minimum="0"
Maximum="100"
Value="{Binding Progress}"
Width="200"
Height="20"
Background="Transparent">
<ProgressBar.Foreground>
<SolidColorBrush Color="White"/>
</ProgressBar.Foreground>
</ProgressBar>
</StackPanel>
进度条用到了ProgressBar 的控件,对应的官方文档地址:ProgressBar
控件的属性:
Property | Description |
---|---|
Minimum |
最大值 |
Maximum |
最小值 |
Value |
当前值 |
Foreground |
进度条颜色 |
ShowProgressText |
显示进度数值 |
Value 值通过Binding绑定了ViewModel中的属性字段Progress。通过UpdateProgress()方法,让Progress的值由0变化到100,模拟加载过程。
代码如下:
private async void UpdateProgress() {
// 模拟登录加载过程
for (int i = 0; i <= 100; i++) {
Progress = i;
await Task.Delay(100); // 延迟一段时间,以模拟加载过程
}
ToMainAction?.Invoke();
}
5.加载完成后跳转首页
界面的跳转,通过Action委托来完成,首先在LoadViewModel中定义 ToMainAction,在上面的UpdateProgress方法完成时执行Invoke,而ToMainAction的实现方法,写在OnFrameworkInitializationCompleted方法中。
ToMainAction的实现方法中,将desktop.MainWindow变更成MainWindow。loadView隐藏,MainWindow显示。
2.首页+API配置
加载界面 是承载程序的界面,界面样式如下:
1.作用和功能:
首页 主要作用是承载程序的界面,每一个Avalonia项目在创建时会自动创建MainWindow.axaml 在界面axaml中很简单。承载了MainView 的用户控件,和API设置界面。
首页 包括控制API设置的数据交互、键盘的监听事件、系统语言的判断。
API配置 包括用于OpenAI接口调用参数的全部设置。
2.界面设计-设置弹框
代码如下:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:TerraMours.Chat.Ava.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="TerraMours.Chat.Ava.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
RenderOptions.BitmapInterpolationMode="HighQuality"
xmlns:sty="using:FluentAvalonia.Styling"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:local="using:TerraMours.Chat.Ava.Views"
Icon="/Assets/terramours.ico"
Title="TerraMours.Chat.Ava">
<dialogHost:DialogHost IsOpen="{Binding ApiSettingIsOpened}"
DialogMargin="16"
DisableOpeningAnimation="True"
dialogHost:DialogHostStyle.CornerRadius="8"
Background="rgb(52, 53, 65)">
<dialogHost:DialogHost.DialogContent>
<local:ApiSettingsView />
</dialogHost:DialogHost.DialogContent>
<Panel>
<local:MainView />
</Panel>
</dialogHost:DialogHost>
</Window>
界面中使用到了 DialogHost.Avalonia,做弹框的简单实现。IsOpen 控制弹窗的显隐性。DialogHost.DialogContent 中填入弹框的显示内容。显示内容为
ApiSettingsView。
而主体部分只有一个Panel,包含着MainView,这使得MainView的界面会占满整个程序界面。至此,首页的界面设计就完成了。
Icon="/Assets/terramours.ico" 设置了程序的logo,如下:
3.初始化
MainWindow 在界面加载时要做很多工作。MainWindow的构造函数如下:
代码如下:
public MainWindow() {
InitializeComponent();
this.Closing += (sender, e) => SaveWindowSizeAndPosition();
this.Loaded += MainWindow_Loaded;
MainWindowViewModel = new MainWindowViewModel();
VMLocator.MainWindowViewModel = MainWindowViewModel;
DataContext = MainWindowViewModel;
var cultureInfo = CultureInfo.CurrentCulture;
if (cultureInfo.Name == "zh-CN") {
Translate("zh-CN");
}
this.KeyDown += MainWindow_KeyDown;
}
MainWindow的构造函数绑定了多个事件的实现方法:
- this.Loaded 界面加载时触发MainWindow_Loaded 方法,作用是加载本地数据和本地配置文件。
- this.Closing 程序关闭时触发SaveWindowSizeAndPosition方法,作用是保存当前的系统设置,包括用户调整后的界面的长宽和在屏幕的位置,用户下次打开时程序还会出现在之前的位置。比如,我把系统拉到桌面左上角,把窗口缩小到最小尺寸时候退出程序,下次打开,程序界面还会在之前退出的位置,在桌面左上角以最小尺寸出现。
- this.KeyDown 监听键盘的输入事件,当按键按下时,会触发MainWindow_KeyDown方法,用于绑定自定义的快捷键。
在MainWindow构造函数中,通过判断CultureInfo.CurrentCulture,获取当前 操作系统的语言系统,判断程序应该显示哪个国家的语言。从而确定程序显示的语言,通过Translate 修改语言相关配置。是系统国际化的实践。
4.系统配置-本地保存和加载
系统配置 对应AppSettings类,记录了应用程序设置和 ChatGPT API参数
系统设置参数通过保存到文件settings.json中实现配置的本地化和持久化。
MainWindow_Loaded 的方法实现系统配置加载:
代码如下:
private async void MainWindow_Loaded(object sender,RoutedEventArgs e) {
var settings = await LoadAppSettingsAsync();
if (File.Exists(Path.Combine(settings.AppDataPath, "settings.json"))) {
this.Width = settings.Width - 1;
this.Position = new PixelPoint(settings.X, settings.Y);
this.Height = settings.Height;
this.Width = settings.Width;
this.WindowState = settings.IsMaximized ? WindowState.Maximized : WindowState.Normal;
}
else {
var screen = Screens.Primary;
var workingArea = screen.WorkingArea;
double dpiScaling = screen.PixelDensity;
this.Width = 1300 * dpiScaling;
this.Height = 840 * dpiScaling;
this.Position = new PixelPoint(5, 0);
}
if (!File.Exists(settings.DbPath)) {
_dbProcess.CreateDatabase();
}
await _dbProcess.DbLoadToMemoryAsync();
await VMLocator.MainViewModel.LoadPhraseItemsAsync();
VMLocator.MainViewModel.SelectedPhraseItem = settings.PhrasePreset;
VMLocator.MainViewModel.SelectedLogPain = "Chat List";
await Dispatcher.UIThread.InvokeAsync(() => { VMLocator.MainViewModel.LogPainIsOpened = false; });
if (this.Width > 1295) {
//await Task.Delay(1000);
await Dispatcher.UIThread.InvokeAsync(() => { VMLocator.MainViewModel.LogPainIsOpened = true; });
}
this.GetObservable(ClientSizeProperty).Subscribe(size => OnSizeChanged(size));
_previousWidth = ClientSize.Width;
await _dbProcess.UpdateChatLogDatabaseAsync();
await _dbProcess.CleanUpEditorLogDatabaseAsync();
if (string.IsNullOrWhiteSpace(VMLocator.MainWindowViewModel.ApiKey)) {
var dialog = new ContentDialog() { Title = $"Please enter your API key.", PrimaryButtonText = "OK" };
await VMLocator.MainViewModel.ContentDialogShowAsync(dialog);
VMLocator.ChatViewModel.OpenApiSettings();
}
}
MainWindow_Loaded 的方法中,通过解析settings.json,加载系统配置。
settings.json 的解析和保存
相关方法如下:
至此,系统配置的开发就基本完成了。对于这些不需要远程同步的基础配置,保存在本地文件中。
5.国际化
通过Translate方法,根据当前系统语言,改变控制文字显示的资源文件,实现语言的切换。
代码如下:
public void Translate(string targetLanguage) {
var translations = App.Current.Resources.MergedDictionaries.OfType<ResourceInclude>().FirstOrDefault(x => x.Source?.OriginalString?.Contains("/Lang/") ?? false);
if (translations != null)
App.Current.Resources.MergedDictionaries.Remove(translations);
App.Current.Resources.MergedDictionaries.Add(
(ResourceDictionary)AvaloniaXamlLoader.Load(
new Uri($"avares://TerraMours.Chat.Ava/Assets/lang/{targetLanguage}.axaml")
)
);
}
关于国际化的资源文件的创建请看前篇内容:基于Avalonia 11.0.0+ReactiveUI 的跨平台项目开发1-通用框架
3.主界面
主界面 是项目的核心,包括了以下图片所有内容的布局,它勾勒出了整个程序。中间包括左上角的图标,会话列表,聊天区域,查询,配置等等。
1.作用和功能
主界面 的作用,是显示和完成整个业务功能的展示和交互。主界面 将会话列表和聊天窗口左右分开。控制了整个程序的排版和布局。
主要分三块:
- 程序标题logo
- 会话列表
- 聊天窗口
2.界面设计
具体代码不贴出来了,需要了解的同学可以fork项目代码查看,功能区已经标注注释了,方便查看。
3.SplitView控制会话列表显示
会话列表和聊天窗口 通过SplitView 实现,会话列表在窗口缩小时自动隐藏。通过IsPaneOpen属性控制。
隐藏效果:
实现方法为OnSizeChanged方法:
代码如下:
private void OnSizeChanged(Size newSize) {
if (_previousWidth != newSize.Width) {
if (newSize.Width <= 1295) {
VMLocator.MainViewModel.LogPainIsOpened = false;
VMLocator.MainViewModel.LogPainButtonIsVisible = false;
}
else {
if (VMLocator.MainViewModel.LogPainButtonIsVisible == false) {
VMLocator.MainViewModel.LogPainButtonIsVisible = true;
}
if (newSize.Width > _previousWidth) {
VMLocator.MainViewModel.LogPainIsOpened = true;
}
}
_previousWidth = newSize.Width;
}
}
当窗口宽度小于1295,会修改VMLocator.MainViewModel.LogPainButtonIsVisible为false,实现会话列表隐藏的效果。
4.初始化
MainViewModel控制了程序大部分的按键的事件实现,MainViewModel的构造函数如下:
代码如下:
public MainViewModel() {
PostButtonText = "Post";
LoadChatListCommand = ReactiveCommand.CreateFromTask<string>(async (keyword) => await LoadChatListAsync(keyword));
PhrasePresetsItems = new ObservableCollection<string>();
//会话
ImportChatLogCommand = ReactiveCommand.CreateFromTask(ImportChatLogAsync);
ExportChatLogCommand = ReactiveCommand.CreateFromTask(ExportChatLogAsync);
DeleteChatLogCommand = ReactiveCommand.CreateFromTask(DeleteChatLogAsync);
//配置
SystemMessageCommand = ReactiveCommand.Create(InsertSystemMessage);
HotKeyDisplayCommand = ReactiveCommand.CreateFromTask(HotKeyDisplayAsync);
OpenApiSettingsCommand = ReactiveCommand.Create(OpenApiSettings);
ShowDatabaseSettingsCommand = ReactiveCommand.CreateFromTask(ShowDatabaseSettingsAsync);
//聊天
PostCommand = ReactiveCommand.CreateFromTask(PostChatAsync);
}
其中,绑定了会话、配置、聊天等功能的按钮事件。实现业务的交互。
5.调用ChatGpt接口
通过Betalgo.OpenAI 完成接口调用,是一个开源的nuget包,集成了OpenAI的接口,简化了调用逻辑。
本来更倾向于Senmantic Kernel的,是微软开发的LLM训练框架,但是代理方面我还没有很好的解决办法,后面再替换。
接口调用方法写在PostChatAsync方法里,通过post按钮发起调用:
代码如下:
/// <summary>
/// OpenAI 调用方法
/// </summary>
/// <returns></returns>
private async Task PostChatAsync()
{
try
{
string message = PostMessage;
int conversationId = 1;
//创建会话
if(VMLocator.DataGridViewModel.ChatList == null)
{
VMLocator.DataGridViewModel.ChatList=new ObservableCollection<ChatList> ();
VMLocator.DataGridViewModel.ChatList.Add(new ChatList() { Id=1,Title=(message.Length< 5?message:$"{message.Substring(0,5)}..."), Category = (message.Length < 5 ? message : $"{message.Substring(0, 5)}...") ,Date=DateTime.Now});
}
if (VMLocator.ChatViewModel.ChatHistory == null)
VMLocator.ChatViewModel.ChatHistory = new ObservableCollection<Models.ChatMessage>();
VMLocator.ChatViewModel.ChatHistory.Add(new Models.ChatMessage() { ChatRecordId = 1, ConversationId = conversationId, Message = message, Role = "User", CreateDate = DateTime.Now });
//根据配置中的CONTEXT_COUNT 查询上下文
var messages = new List<OpenAI.ObjectModels.RequestModels.ChatMessage>();
messages.Add(OpenAI.ObjectModels.RequestModels.ChatMessage.FromUser(message));
var openAiOpetions = new OpenAI.OpenAiOptions()
{
ApiKey = AppSettings.Instance.ApiKey,
BaseDomain = AppSettings.Instance.ApiUrl
};
var openAiService = new OpenAIService(openAiOpetions);
//调用SDK
var response = await openAiService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest
{
Messages = messages,
Model = AppSettings.Instance.ApiModel,
MaxTokens = AppSettings.Instance.ApiMaxTokens,
});
if (response == null)
{
var dialog = new ContentDialog()
{
Title = "接口调用失败",
PrimaryButtonText = "Ok"
};
await VMLocator.MainViewModel.ContentDialogShowAsync(dialog);
}
if (!response.Successful)
{
var dialog = new ContentDialog()
{
Title = $"接口调用失败,报错内容: {response.Error.Message}",
PrimaryButtonText = "Ok"
};
await VMLocator.MainViewModel.ContentDialogShowAsync(dialog);
}
VMLocator.ChatViewModel.ChatHistory.Add(new Models.ChatMessage() { ChatRecordId = 2, ConversationId = conversationId, Message = response.Choices.FirstOrDefault().Message.Content, Role = "Assistant", CreateDate = DateTime.Now });
VMLocator.MainViewModel.PostMessage = "";
}
catch (Exception e)
{
}
}
通过创建OpenAIService初始化,Completion接口调用时使用openAiService.ChatCompletion.CreateCompletion方法。
ChatMessage是上下文的模型,通过创建messages完成上下文的创建,请求参数都写在ChatCompletionCreateRequest之中。
目前的第一版使用的CreateCompletion是直接返回的结果。后面我会优化调用,使用Stream流式输出。
4.会话列表
会话列表是模拟chatgpt官网的样式,将聊天按会话的形式归类。chatgpt官网截图如下:
1.作用和功能
会话列表将聊天按会话的形式归类,更好的管理聊天内容。
2.界面设计
因为考虑到后面会有其他类型的AI 类型,决定通过DataGrid实现会话列表,DataGrid的表格类型也能更多的展示数据。
代码如下:
<UserControl xmlns="https://github.com/avaloniaui"
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:vm="using:TerraMours.Chat.Ava.ViewModels"
x:DataType="vm:DataGridViewModel"
xmlns:local="using:TerraMours.Chat.Ava"
x:Class="TerraMours.Chat.Ava.Views.DataGridView">
<UserControl.Resources>
<local:CustomDateConverter x:Key="CustomDateConverter" />
</UserControl.Resources>
<Grid>
<DataGrid Name="ChatListDataGrid"
ItemsSource="{Binding ChatList}"
AutoGenerateColumns="False"
HeadersVisibility="None"
SelectionMode="Single"
SelectedItem="{Binding SelectedItem}"
SelectedIndex="{Binding SelectedItemIndex}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Id}" IsVisible="False"/>
<DataGridTextColumn Foreground="rgb(155,155,155)"
FontSize="12"
Binding="{Binding Date,Converter={StaticResource CustomDateConverter},Mode=OneWay}"
IsReadOnly="True"/>
<DataGridTextColumn Binding="{Binding Title}" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>
会话列表的数据共有三个:Id,创建时间,会话标题。通过DataGridTextColumn 通过绑定的形式实现。
其中,使用了CustomDateConverter 时间转换器,将Date的显示格式做转换。
3.数据交互
会话列表目前还在优化,第一版是通过第一次调用PostChatAsync时创建的。目前的数据存在本地SQLite数据库中。
5.聊天窗口
聊天窗口是程序的工作重心,是展示聊天成果的重要界面。其中用到Markdown.Avalonia的扩展包实现Markdown内容的展示。
1.作用和功能
数据是核心,聊天窗口是数据的展示平台,作用不容小嘘。通过编写数据模板DataTemplate来控制内容的展示。呈现chat问答式的结果。
2.Markdown 风格样式
通过DataTemplate来设置Markdown 风格样式。代码如下:
<DataTemplate>
<Border
Name="MessageBorder"
Background="{Binding Role, Converter={StaticResource ChatBackgroundConverter}}"
HorizontalAlignment="Left"
Padding="5"
Margin="20,5,20,20"
CornerRadius="8,8,8,0">
<md:MarkdownScrollViewer
VerticalAlignment="Stretch"
MarkdownStyleName="Standard"
SaveScrollValueWhenContentUpdated="True"
TextElement.FontSize="16"
TextElement.Foreground="White"
Markdown="{Binding Message}">
<md:MarkdownScrollViewer.Styles>
<Style Selector="ctxt|CCode">
<Style.Setters>
<Setter Property="BorderBrush" Value="Green"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="MonospaceFontFamily" Value="Meiryo" />
<Setter Property="Foreground" Value="DarkGreen" />
<Setter Property="Background" Value="LightGreen" />
</Style.Setters>
</Style>
<Style Selector="Border.CodeBlock">
<Style.Setters>
<Setter Property="BorderBrush" Value="#E2E6EA" />
<Setter Property="BorderThickness" Value="0,30,0,0" />
<Setter Property="Margin" Value="5,0,5,0" />
<Setter Property="Background" Value="Black" />
</Style.Setters>
</Style>
<Style Selector="TextBlock.CodeBlock">
<Style.Setters>
<Setter Property="Background" Value="Black" />
</Style.Setters>
</Style>
<Style Selector="avedit|TextEditor">
<Style.Setters>
<Setter Property="BorderBrush" Value="#E2E6EA" />
<Setter Property="Background" Value="Black" />
<Setter Property="Padding" Value="5"></Setter>
</Style.Setters>
</Style>
</md:MarkdownScrollViewer.Styles>
<md:MarkdownScrollViewer.ContextMenu>
<ContextMenu Padding="3">
<MenuItem>
<MenuItem.Header>
<TextBlock>编辑</TextBlock>
</MenuItem.Header>
</MenuItem>
<!--<MenuItem Tag="{Binding ChatRecordId}" Click="DeleteClick">
<MenuItem.Header>
<TextBlock>删除</TextBlock>
</MenuItem.Header>
</MenuItem>
<MenuItem Tag="{Binding Message}" Click="CopyClick">
<MenuItem.Header>
<TextBlock>复制</TextBlock>
</MenuItem.Header>
</MenuItem>-->
</ContextMenu>
</md:MarkdownScrollViewer.ContextMenu>
</md:MarkdownScrollViewer>
</Border>
</DataTemplate>
MarkdownScrollViewer.Styles 根据不同的内容设置不同的样式。
MarkdownScrollViewer.ContextMenu设置右键菜单。
其中通过ChatBackgroundConverter转换器根据角色控制背景,ChatBackgroundConverter代码如下:
3.总结和待办事项
avalonia开发目前网上,特别是国内的网站的教程和文章很少,希望能给大家一点学习使用avalonia开发客户端项目的朋友一点帮助。写的不对的地方也恳请大家多多留言,我会及时更正,多多交流心得体会。
Todo:
- 项目发布,在多平台下的运行
- 搭建国产系统虚拟机测试avalonia项目
- 程序改造成云同步版本,跟我做的web项目互通。
- 优化UI界面
- 优化语言国际化内容
**目前程序还没有完全开发完成。后续的开发我会及时跟进。阅读如遇样式问题,请前往个人博客浏览:https://www.raokun.top
目前web端ChatGPT:https://ai.terramours.site
当前开源项目地址:https://github.com/raokun/TerraMours.Chat.Ava
基于Avalonia 11.0.0+ReactiveUI 的跨平台项目开发2-功能开发的更多相关文章
- DotNetBar for Windows Forms 11.8.0.8冰河之刃重打包版
关于 DotNetBar for Windows Forms 11.8.0.8_冰河之刃重打包版 基于 官方原版的安装包 + http://www.cnblogs.com/tracky 提供的补丁DL ...
- 基于Hadoop 2.2.0的高可用性集群搭建步骤(64位)
内容概要: CentSO_64bit集群搭建, hadoop2.2(64位)编译,安装,配置以及测试步骤 新版亮点: 基于yarn计算框架和高可用性DFS的第一个稳定版本. 注1:官网只提供32位re ...
- Oracle Linux 6.3下安装Oracle 11g R2(11.2.0.3)
本文主要描写叙述了在Oracle Linux 6.3下安装Oracle 11gR2(11.2.0.3).从Oracle 11g開始,Oracle官方站点不再提供其Patch的下载链接,须要使用Meat ...
- 【实战】静默安装-oracle 11.2.0.3 on centos 5.10
发现网上静默安装的文章非常多,乱七八糟,五花八门!来个扫盲的! centos 5.10 下安装oracle 11g_r2 ************************************* ...
- Kafka 0.11.0.0 实现 producer的Exactly-once 语义(中文)
很高兴地告诉大家,具备新的里程碑意义的功能的Kafka 0.11.x版本(对应 Confluent Platform 3.3)已经release,该版本引入了exactly-once语义,本文阐述的内 ...
- 【转】:Oracle Linux6.9下安装Oracle 11.2.0.4.0及psu补丁升级
为方便截图,本文操作都在vmware虚拟机上完成. 目录: 1.操作系统安装 2.数据库安装 3.PSU补丁升级卸载 part1 操作系统安装 Oracle (Enterprise) Linux ...
- 基于restful注解(spring4.0.2整合flex+blazeds+spring-mvc)<一>
摘自: http://www.blogjava.net/liuguly/archive/2014/03/10/410824.html 参考官网:1.http://livedocs.adobe.com/ ...
- 基于数据库的代码自动生成工具,生成JavaBean、生成数据库文档、生成前后端代码等(v6.0.0版)
TableGo v6.0.0 版震撼发布,此次版本更新如下: 1.UI界面大改版,组件大调整,提升界面功能的可扩展性. 2.新增BeautyEye主题,界面更加清新美观,也可以通过配置切换到原生Jav ...
- 1级搭建类102-Oracle 11g 单实例 FS(11.2.0.4+RHEL 7)公开
项目文档引子系列是根据项目原型,制作的测试实验文档,目的是为了提升项目过程中的实际动手能力,打造精品文档AskScuti. 项目文档引子系列目前不对外发布,仅作为博客记录.如学员在实际工作过程中需提前 ...
- 基于.Net Core 5.0 Worker Service 的 Quart 服务
前言 看过我之前博客的人应该都知道,我负责了相当久的部门数据同步相关的工作.其中的艰辛不赘述了. 随着需求的越来越复杂,最近windows的计划任务已经越发的不能满足我了,而且计划任务毕竟太弱智,总是 ...
随机推荐
- RabbitMQ详解(上)
一:MQ的相关概念 MQ(message queue),从字面意思上看,本质是个队列,FIFO 先入先出,只不过队列中存放的内容是message 而已,还是一种跨进程的通信机制,用于上下游传递消息.在 ...
- 21-HMR
/* HMR:hot module replacement 热模块替换 / 模块热替换 作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块) 极大提升构建速度 样式文件:可以使用HMR ...
- 05-打包样式资源(编写webpack配置文件)
/** * webpack.config.js webpack的配置文件 * 作用:指示 webpack 干哪些活(当你运行 webpack 指令时,会加载里面的配置) * * 所有构件工具都是基于n ...
- 使用ServiceSelf解决.NET应用程序做服务的难题
1 ServiceSelf 为.NET 泛型主机的应用程序提供自安装为服务进程的能力,支持windows和linux平台. 功能 自我服务安装 自我服务卸载 自我服务日志监听 2 自我服务安装 虽然. ...
- react中refs详解
https://zh-hans.reactjs.org/docs/refs-and-the-dom.html 字符串形式ref 1 <input ref="myinput" ...
- 关于聚合根,领域事件的那点事---深入浅出理解DDD
作者:京东物流 赵勇萍 前言 最近有空会跟同事讨论DDD架构的实践落地的情况,但真实情况是,实际中对于领域驱动设计中的实体,值对象,聚合根,领域事件这些战术类的实践落地,每个人理解依然因人而异,大概率 ...
- Prism Sample 11-UsingDelegateCommands
本例的知识点,全在ViewModel中,看代码: 1 public class MainWindowViewModel : BindableBase 2 { 3 private bool _isEna ...
- React笔记-组件通信(六)
React笔记-组件通信(六) props概念 props是组件之间通讯的纽带 props也是组件中内置的一个属性 通过父级组件传入 在类组件里 可以直接通过this.props获取 注意: prop ...
- JAVA 23种设计模式(小白进阶必经之路)
如今几乎所有程序都遵循万物皆对象的开发理念,然在写程序中我们用的最多的应该是封装(encapsulation).继承(inheritance).多态(Polymorphism)开发模式:而更高一个境界 ...
- odoo开发教程十一:视图综述
一:视图标签等公共结构 name (必选) 用于通过名字查找标签 model: 与view相关联的model priority 当搜索查找view时,优先级最低的view会被返回 arch 视图lay ...