跨平台开发最令人头疼的是在一个平台上能正常运行,换到另外一个平台就不行了,尤其是那些细小的功能,来回的代码修改、重新编译、发布运行着实令人崩溃。
在桌面操作系统中,对文件的选择、查找是很常见的操作,因此系统对话框算是一个桌面UI库最基本的需求了。来看看通过解决一个小小对话框的问题又学到了什么。
1.Avalonia中的系统对话框
Avalonia中的系统对话框都是派生自抽象类SystemDialog,位于Avalonia.Controls命名空间下,公共属性有Title“标题”。文件操作相关的对话框都派生自抽象类FileSystemDialog,公共属性为初始路径。
以打开文件对话框OpenFileDialog为例,可以设置初始文件名称、文件类型过滤器、是否可选多个文件;打开对话框的方法为ShowAsync(),输入参数为对话框的父级窗口,必填;返回参数为字符串数组,即选中文件的完整路径的集合,如果取消选择则返回null。
ShowAsync(Window parent)的实现为:
/// <summary>
/// Shows the open file dialog.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <returns>
/// A task that on completion returns an array containing the full path to the selected
/// files, or null if the dialog was canceled.
/// </returns>
public Task<string[]?> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
var service = AvaloniaLocator.Current.GetService<ISystemDialogImpl>() ??
throw new InvalidOperationException("Unable to locate ISystemDialogImpl.");
return service.ShowFileDialogAsync(this, parent);
}
使用方法如下:
这不是最佳写法,后面再附上完整示例代码
private void SelectStationFile()
{
var dialog = new OpenFileDialog
{
Title = "请选择文件"
};
var result = dialog.ShowAsync(Views.MainWindow.Instance);
if (result.Result != null)
{
var filePath= result.Result[0];
}
}
2.跨平台调试的痛
开发环境是Window10、AMD处理器、VisualStudio2022,测试环境是统信UOS、Windows10,CPU均有AMD5000系列和Intel i7系列。
就是打开对话框选择文件这个小小的功能,Windows环境顺畅运行,统信UOS时好时坏,大多数版本都无法正常打开对话框,点击打开对话框的按钮后,应用即进入卡死状态,甚至强制退出都不好使,只能重启电脑。
多次调整代码还是有问题,考虑问题出现的原因可能涉及几方面:
- UI渲染库:不同的平台使用不同UI渲染库的差异;
- MVVM模式:MVVM模式默认使用的是ReactiveUI,普通模式则没有限定;
- UI线程:打开对话框是异步方法,不在主线程内,线程堵塞;
基于上面思路排查问题的过程中,又了解到Avalonia中日志和异常处理的内容。
3.涨知识涨经验
3.1.日志跟踪
Avalonia使用System.Diagnostics.Trace进行日志跟踪,可配置日志跟踪的级别和范围。
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace(LogEventLevel.Debug, LogArea.Property, LogArea.Layout);
也可引用第三方类库将日志输出到控制台或者文件,官方文档中使用的是Serilog
3.2.异常处理
在Program.cs的Main方法中可以进行全局异常捕获。
[STAThread]
public static void Main(string[] args)
{
//日志类库使用Serilog
Log.Logger=new LoggerConfiguration().WriteTo.File("log.txt",rollingInterval:RollingInterval.Day).CreateLogger();
try
{
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
catch (Exception ex)
{
Log.Fatal(ex, "Unhandled Exception");
}
finally
{
Log.CloseAndFlush();
}
}
- 多线程任务相关的异常需要安装TaskScheduler.UnobservedTaskException进行捕获。
- ReactiveUI相关的异常需要在代码中订阅RxApp.DefaultExceptionHandler进行捕获。
3.3.访问UI线程
与WPF类似,Avalonia访问主UI线程使用Dispatcher,可以在非UI线程中访问界面对象,位于Avalonia.Threading命名空间下,用法如下:
private void SelectStationFile()
{
var dialog = new OpenFileDialog
{
Title = "请选择文件"
};
var result = dialog.ShowAsync(Views.MainWindow.Instance);
if (result.Result != null)
{
Dispatcher.UIThread.Post(() =>
{
_stationSourcePath = result.Result[0];
});
}
}
4.原因竟是这样
按照前面的思路排查下来竟然还没有解决,百无聊赖之下将绑定命令调用的方法改成async异步,竟然能行了。
这是正确的代码,能够正常工作
private async void SelectClimateFile()
{
try
{
var dialog = new OpenFileDialog
{
Title = "请选择文件"
};
var result = await dialog.ShowAsync(Views.MainWindow.Instance);
if (result != null)
{
ClimateSourcePath = result[0];
}
}
catch (Exception ex)
{
var message = MessageBox.Avalonia.MessageBoxManager.GetMessageBoxStandardWindow("Error", ex.Message);
message.Show();
}
}
回想起来,从前后端命令的定义、绑定到基类中打开对话框的实现,都是使用多线程Task,异步的操作应该是必要的。至于为什么使用同步方法时Windows环境能正常工作,Linux环境就不行的原因,还是要更深入的了解底层原理。
只有扎实的基础,才能更快看清问题的本质。