A Journey of Migrating a WPF Project to UNO Framework: Triumphs and Challenges #14548
Replies: 2 comments 1 reply
-
Thanks so much for taking the time to document it. We'd love to team up to touch up our docs where needed in order to make this a great experience for everyone. If you don't mind, please reach out to sasha at platform.uno |
Beta Was this translation helpful? Give feedback.
-
Notes on Migrating from WPF to UNO under UOSThis article documents my experience in migrating a small WPF application to the UNO framework for support on the UOS(统信). Before I begin, let me explain my requirements. I currently have a small WPF application that I need to run on both the UnionTech OS and Windows. As we all know, there are many multi-platform development frameworks available in the current dotnet system. This time, I decided to try developing with the UNO/MAUI approach. The overall technical architecture is shown in the diagram below. As shown in the diagram, I still use the WPF framework on Windows. However, this time the WPF framework is used as the underlying framework. Most of the business code will not directly touch the WPF framework, only some platform compatibility adaptation code will. The rest of the business code will indirectly use the WPF framework through the UNO and MAUI frameworks. On the UOS, the GTK application framework is used. Similarly, only platform compatibility adaptation code will touch the GTK application framework, and most business code will not directly interact with it. The overall rendering layer uses SKIA to ensure consistent rendering effects across multiple platforms. Daily DevelopmentWhen creating a new project, remember to check the Windows project option. This will generate a WinUI3 project. When writing code, choose the WinUI 3 project to get XAML code intelligent prompts. When debugging, prioritize using the WinUI 3 project to debug the interface layout. You can directly use Visual Studio's hot reload support for WinUI 3, which works better. I recommend also adding the Skia.WPF and Skia.GTK projects. GTK can run on both Windows and Linux systems, but GTK may have some strange issues on Windows. In this case, switch to Skia.WPF. After all, those really released on the Windows platform won't be so desperate to use GTK as the underlying layer. TextFlickering Black Screen on UOSThis is an OpenGL issue. For the fix, please see #13530. Chinese Text GarbledChinese text garbled is due to the incorrect loading of Chinese fonts. This problem also applies to languages such as Korean or Japanese. UOS has the Source Han Sans font by default, and GTK will automatically roll back the font. All you need to do is set the application to use Microsoft YaHei. Setting it to Microsoft YaHei will allow the application to display normal sans-serif fonts on both Windows and UOS systems. The setting method is as follows: <TextBlock Text="解决 UOS 中文乱码" FontFamily="Microsoft YaHei UI"/> Remember to use the Addition: Uno with Wpf Chinese code display messy code · Issue #6973 · unoplatform/uno TextBox Stretching SpaceIf there is content that depends on the space stretched by the measurement during the TextBox input process, then the stretched space may be incorrect, such as the following code: <TextBox HorizontalAlignment="Center" FontSize="50"></TextBox> With this logic, you will see the text content being clipped during the input process. Basically, you can see the text content being clipped under the Skia.WPF and Skia.GTK projects. For now, the only workaround is to modify the interface design. TextBox's Minimum HeightThe minimum height will still be higher than expected, so you can only modify the interface design to work around it. TextBox's Scroll BarFor example, to scroll to the bottom, you can use the following code: private void ScrollToBottom(TextBox textBox)
{
//textBox.Spy();
if(textBox.VisualDescendant<ScrollViewer>() is { } scrollViewer)
{
scrollViewer.ChangeView(0.0f, scrollViewer.ExtentHeight, 1.0f, true);
}
} This VisualDescendant method is an auxiliary method, the code is as follows: static class TreeExtensions
{
public static T? VisualDescendant<T>(this UIElement element) where T : DependencyObject
=> VisualDescendant<T>((DependencyObject) element);
public static T? VisualDescendant<T>(DependencyObject element) where T : DependencyObject
{
if (element is T)
{
return (T) element;
}
T? foundElement = default;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
var child = VisualTreeHelper.GetChild(element, i);
foundElement = VisualDescendant<T>(child);
if (foundElement != null)
{
break;
}
}
return foundElement;
}
} This method is also useful for ListView and others. The core is to find the ScrollViewer object through the visual tree and control the scrolling through the ScrollViewer. StreamGeometry Resources for Geometric ShapesIn WPF, icons often use Path geometric paths as vector icons, which are put into StreamGeometry resources. StreamGeometry resources made from a single Path can be replaced in UNO with <StreamGeometry x:Key="Geometry.Close">
M18.363961,5.63603897 C18.7544853,6.02656326 18.7544853,6.65972824 18.363961,7.05025253 L13.4142136,12 L18.363961,16.9497475 C18.7544853,17.3402718 18.7544853,17.9734367 18.363961,18.363961 C17.9734367,18.7544853 17.3402718,18.7544853 16.9497475,18.363961 L12,13.4142136 L7.05025253,18.363961 C6.65972824,18.7544853 6.02656326,18.7544853 5.63603897,18.363961 C5.24551468,17.9734367 5.24551468,17.3402718 5.63603897,16.9497475 L10.5857864,12 L5.63603897,7.05025253 C5.24551468,6.65972824 5.24551468,6.02656326 5.63603897,5.63603897 C6.02656326,5.24551468 6.65972824,5.24551468 7.05025253,5.63603897 L12,10.5857864 L16.9497475,5.63603897 C17.3402718,5.24551468 17.9734367,5.24551468 18.363961,5.63603897 Z
</StreamGeometry> In WPF, suppose it is set on a button as an icon button, you can define a style, the content is roughly as follows: <Style x:Key="Style.TitlebarButton" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="#808080" />
<Setter Property="Width" Value="24"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ButtonBase}">
<Grid Background="{TemplateBinding Background}" UseLayoutRounding="True">
<Path Fill="{TemplateBinding Foreground}"
Data="{Binding Path=Content, RelativeSource={RelativeSource TemplatedParent}}">
</Path>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter> The code used in the specific business is roughly as follows: <Button Style="{StaticResource Style.TitlebarButton}" Content="{StaticResource Geometry.Close}"/> After moving to UNO, change the StreamGeometry type resource to an <x:String x:Key="Geometry.Close">M18.363961,5.63603897 C18.7544853,6.02656326 18.7544853,6.65972824 18.363961,7.05025253 L13.4142136,12 L18.363961,16.9497475 C18.7544853,17.3402718 18.7544853,17.9734367 18.363961,18.363961 C17.9734367,18.7544853 17.3402718,18.7544853 16.9497475,18.363961 L12,13.4142136 L7.05025253,18.363961 C6.65972824,18.7544853 6.02656326,18.7544853 5.63603897,18.363961 C5.24551468,17.9734367 5.24551468,17.3402718 5.63603897,16.9497475 L10.5857864,12 L5.63603897,7.05025253 C5.24551468,6.65972824 5.24551468,6.02656326 5.63603897,5.63603897 C6.02656326,5.24551468 6.65972824,5.24551468 7.05025253,5.63603897 L12,10.5857864 L16.9497475,5.63603897 C17.3402718,5.24551468 17.9734367,5.24551468 18.363961,5.63603897 Z</x:String> The rest of the code is basically the same as WPF, as in the following UNO button style <Style x:Key="Style.TitlebarButton" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="#808080" />
<Setter Property="Width" Value="24"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ButtonBase">
<Grid Background="{TemplateBinding Background}">
<Path Fill="{TemplateBinding Foreground}" Data="{TemplateBinding Content}"></Path>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style> Then you can see the button code define is same as the WPF code: <Button Style="{StaticResource Style.TitlebarButton}" Content="{StaticResource Geometry.Close}"/> PathGeometrySome parts are not supported, so multi-platform testing is required. You may need to find a workaround. x:StaticStatic binding is not supported, so a workaround is necessary. For example, you can redefine an instance property that references the static value, and then bind to the instance property. Alternatively, you can move some static properties to the resource dictionary. For instance, in WPF, you would write it like this: public static class BooleanToVisibility
{
public static IValueConverter CollapsedWhenTrue { get; private set; } = new VisibilityConverter
{
Visible = false,
Collapsed = true
};
}
<Border Visibility="{Binding Foo, Converter={x:Static uiConverters:BooleanToVisibility.CollapsedWhenTrue}}"/> In UNO, you would modify it to use the resource dictionary: <UserControl.Resources>
<uiConverters:VisibilityConverter x:Key="CollapsedWhenTrue" Visible="False" Collapsed="True"/>
</UserControl.Resources>
<Border Visibility="{Binding Foo, Converter={StaticResource CollapsedWhenTrue}}"> Image ResourcesImage resources can use relative or absolute paths. The format for absolute paths in UNO is as follows: <Image Source="ms-appx:///[MyApp]/Assets/MyImage.png" /> The By default, all images are referenced as <Content Include="Assets\**;**/*.png;**/*.bmp;**/*.jpg;**/*.dds;**/*.tif;**/*.tga;**/*.gif" Exclude="bin\**;obj\**;**\*.svg" /> Newly added image files do not require any modifications by default. However, for platform compatibility, I recommend using png, jpg, and bmp formats, as all platforms support these formats. If your image does not display, please follow these steps:
Images can be used as content in the resource dictionary. You can use the BitmapImage type, which is the same as in WPF. However, the content of the Source needs to be changed under the absolute path, as shown in the following example: <BitmapImage x:Key="Image.Logo.Size24" UriSource="ms-appx:///[MyApp]/Assets/Logo/logo24x24.png"></BitmapImage> For more information, please refer to the official documentation Assets and image display ContentControlThe functionality aligns with WPF, but the default style behavior is different. The default HorizontalContentAlignment and VerticalContentAlignment are in the top left corner. You need to set them to Stretch to align with WPF. <ContentControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"></ContentControl> Default Control PropertiesMost control default properties are the same as in WPF. However, a few layout properties are different. For example, many controls' HorizontalAlignment and VerticalAlignment are in the top left corner. You need to set them to Stretch to align with WPF. Changes to csprojDue to some conflicts between UNO and VisualStudio, creating a new file may cause UNO's csproj to add unnecessary code. During the development process, before uploading to git, check whether the changes to csproj are necessary. If the changes are unnecessary, please revert them. Generally, you need to revert the changes to csproj after creating a new file, such as creating a new type or user control. DispatcherThe Dispatcher in UNO is weaker than that in WPF, but some replacements can be made. The logic of obtaining the Dispatcher from the original interface elements remains unchanged. The logic of obtaining it statically, such as the following WPF code, needs to be replaced. System.Windows.Application.Current.Dispatcher.InvokeAsync The method of obtaining the static main thread dispatcher from UNO is the same as that of UWP or WinUI 3, as shown in the following code. await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
// Write the dispatch implementation code here
}); Unlike WPF's Dispatcher scheduling level, UNO's schedulable level is very limited, with only the following schedulable levels.
In most cases, the Normal priority is used. However, when running WinUI 3, the acquisition of the A more secure way is to store the Microsoft.UI.Dispatching.DispatcherQueue in the App yourself, so that you can get the same DispatcherQueue object obtained from the main UI thread and use it on WinUI 3, WPF and GTK projects. In the WinUI 3 project, the MainWindow.Dispatcher property is still null, which is why the DispatcherQueue is used. public class App : EmbeddingApplication
{
protected async override void OnLaunched(LaunchActivatedEventArgs args)
{
// 忽略其他代码
MainWindow = builder.Window;
#if DEBUG
MainWindow.EnableHotReload();
#endif
Dispatcher = MainWindow.DispatcherQueue;
Host = await builder.NavigateAsync<Shell>();
}
public Microsoft.UI.Dispatching.DispatcherQueue Dispatcher { private set; get; } = null!;
} Missing MechanismsVisibility.HiddenThere is no hidden option, instead, you can set the opacity to 0. Setting MultiBindingMultiBinding is not supported, so you have to find a workaround and write the interface with only single binding. ControlTemplate.TriggersThis is not supported, a workaround is needed. Use of x:Name attribute in ResourcesThe use of x:Name in resources is not supported. This is because x:Name must be assigned a property or field when it is generated, but resources can be created multiple times, which the generated code cannot handle. This issue was previously raised by Avalonia's XAML creator, and now WinUI 3, UNO, and MAUI all have this problem. The simplest reproduction code is as follows: <Page.Resources>
<ResourceDictionary>
<SolidColorBrush x:Name="MyBrush" Color="Blue"/>
</ResourceDictionary>
</Page.Resources> In this case, you should use In addition, for the binding logic within resources, such as the following code, you can only find a workaround for this kind of code: <Page.Resources>
<ControlTemplate x:Key="Template.Loading" TargetType="ContentControl">
<Grid x:Name="RootGrid" />
</ControlTemplate>
</Page.Resources> The error message for the above code is For more information, please see Adding Name to a Resource fails on build · Issue #1427 · unoplatform/uno. IPCKnown issue: dotnet-campus/dotnetCampus.Ipc#139 References【Chinese Blog】 WPF uses MAUI's custom drawing logic I share a video of an app which created with uno · unoplatform/uno · Discussion #4962 |
Beta Was this translation helpful? Give feedback.
-
I am thrilled to share my recent development experience of migrating an existing WPF project to the UNO Framework. I continued to use the WPF framework with UNO for running on Windows, but also utilized the GTK framework with UNO for testing the application's behavior on the UOS Linux system.
I found the UNO Framework to be highly effective, despite encountering some display issues when running on Linux. The framework I used was .NET 8, and the migration process took me a week. I documented the challenges I faced during the migration in a blog post, which is currently written in Chinese. If you find value in my blog, I would be more than happy to create an English version of it.
For a detailed account of my migration process and the changes I made, please visit: dotnet-campus/dotnetCampus.Ipc#142
To understand the problems I encountered during the migration and how I solved them, please refer to my Chinese blog post: https://blog.lindexi.com/post/%E4%BB%8E-WPF-%E6%90%AC%E8%BF%81%E5%88%B0-UOS-%E4%B8%8B%E7%9A%84-UNO-%E7%9A%84%E7%AC%94%E8%AE%B0.html
I want to express my heartfelt gratitude to the UNO team and all the contributors to the UNO Framework. Your work has made my development journey a lot smoother and more exciting. Thank you!
Beta Was this translation helpful? Give feedback.
All reactions