Bringing 12-year-old WPF code to .NET Core 3
Back around 2007 I was writing woodworking CAD code as a side project. I called it Sawdust.
Sawdust consists of a WPF app (called "wpfview"), and a solid modeling library (called "sd"), all written in C#. The tools of the day were .NET 2.0 and Visual Studio 2005. WPF was young. NUnit had happened, but NuGet didn't exist yet.
(Update: When I open my 2007 sln file in Visual Studio 2017, it says the projects are targeting .NET 2.0, so that's what I wrote for this blog entry. However, WPF did not come out until .NET 3.0. So I'm actually not sure which is correct.)
I used cygwin, and I would have fallen off my chair if you let me look into the future and see WSL.
In the end, Sawdust never became anything more than a side project, but it was fun, and I learned a lot.
Recently I did some archaeology and dug up the code. With a little work, I've gotten it running under .NET Core 3 Preview 3. Today I posted it to GitHub (open source, Apache License 2).
Overall, I was surprised how easy it was to bring the code into the modern age.
Migrating the code
The code was originally structured to keep the core code ("sd") separate from the WPF-specific parts ("wpfview"). To get things going under .NET Core, I was hoping I could just throw out the old verbose .csproj files, create one new library project and one new WPF project, and drop in the .cs files. I envisioned myself opening a command line and doing something like this:
mkdir sawdust cd sawdust mkdir sd dotnet new classlib del Class1.cs (copy all the core stuff in here) cd .. mkdir wpfview dotnet new wpf del *.xaml *.cs (copy all the WPF-specific stuff in here) dotnet add reference ..\sd\sd.csproj dotnet run
In reality, it wasn't quite that simple, but it was close.
The core code
The first thing I discovered is that my core library wasn't as clean as I remembered. When I tried to build it for .NET Standard 2.0, I immediately found one place where a dependency on the WPF Color type had weaseled its way in.
I don't remember how that happened, but I include this in the story here mostly to highlight how easy it was to find the problem with modern tooling. Here is the entire sd.csproj file:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> </Project>
In contrast, the original sd.csproj was over 200 lines of MSBuild's usual liturgy. And sure enough, it contained a reference to WPF's PresentationCore.
Anyway, before I went any further, I made some minor changes to fix the problem.
Then I deleted the AssemblyInfo.cs file, since dotnet tooling creates that stuff automatically.
After that, all of sd compiled with no other changes.
The WPF code
The WPF app was also pretty simple. I created a .NET Core 3 WPF project with:
dotnet new wpf
which gave me the following wpfview.csproj file:
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <UseWPF>true</UseWPF> </PropertyGroup> </Project>
Again, no MSBuild drama. Very nice.
So I deleted the App and MainWindow stuff generated from the project templated, copied in all my WPF-specific code, deleted AssemblyInfo.cs, and tried to build.
And it almost worked. Unfortunately, one of the types in the sd library was called Action. That wasn't a problem back in 2007, but here in 2019 that resulted in a few places where the compiler couldn't figure out whether I was referring to sd.Action or System.Action, the delegate type introduced in .NET 3.5.
So I had to fix this in a few places by using the full name of my type:
Dictionary<sd.Action, int> ax = new Dictionary<sd.Action, int>();
After that change, I typed dotnet run and everything "Just Worked". I could zoom and rotate the model. The UI controls worked. I even tried tapping the button to copy the 3D model as an image to the clipboard, and then opened paint.net and pasted it.
To be fair, I did eventually get it to fail. I brought up a complicated 3D model and tried to Print. The app looked like it was stressing out for about 30 seconds and then it crashed.
Still, I was amazed. Here I have a WPF app, with over 20,000 lines of 12-year-old C#, building and running with minimal effort on .NET Core 3, without Visual Studio involved at all. Sweet.
I didn't mention them earlier, but the sd library also has a bunch of NUnit tests. Back in 2007, these tests lived inside in the sd library, but this time I wanted to get them out into a library of their own. So when I created the .NET Standard version of sd as described above, I excluded the test case code files.
As I said, Sawdust was written several years before NuGet, so I chuckled when I looked in the original csproj and was reminded of the days that using NUnit required this:
<Reference Include="nunit.framework"> <Name>nunit.framework</Name> <HintPath>..\..\Program Files\NUnit 2.2.6\bin\nunit.framework.dll</HintPath> </Reference>
Anyway, things are much better nowadays. Once again, I used a dotnet project template as a starting point, typing this:
dotnet new nunit
which gave me the following sd_tests.csproj file:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.0</TargetFramework> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <PackageReference Include="nunit" Version="3.11.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> </ItemGroup> </Project>
From here, all I had to do was:
- Add a reference to ..\sd\sd.csproj
- Drop in all the source files for my test cases
- Add the InternalsVisibleTo attribute into sd
- dotnet test
Starting test execution, please wait... Total tests: 233. Passed: 233. Failed: 0. Skipped: 0. Test Run Successful. Test execution time: 6.9188 Seconds
No code changes were required.
More on the solid modeling library
Basically, it's a boundary-representation solid modeling engine. B-rep modelers represent a solid as a bunch of faces, where each face is a 2D polygon, with all its vertices in the same plane.
The relationship between two faces that share an edge is tricky, but a long time ago somebody figured out a clever solution called a half-edge, where each face owns half of the edge, and the two halves know about each other.
To be a valid solid, the list of polygonal faces must follow a few rules. Everything needs to be fully connected. An edge has to be shared by exactly two faces, etc.
Of course, it is important to know the difference between the inside of a solid and the outside. How is this possible if our representation is just a bunch of polygons? The answer is that, by convention, the front of a face (to the outside of the solid) is the one where the vertices are listed counterclockwise.
It is also possible for a face to have one or more holes in it. Those holes are also represented as polygons, except with the vertices listed in clockwise order, the opposite of the face itself.
This representation is the foundation upon which we can build all kinds of things, including the ability to do boolean operations on solids. The sd library can do things like use one solid to cut away part of another.
Beyond the usual B-rep stuff, this code has been augmented with a few woodworking-specific features. For example, when a board is created, it has a vector indicating the direction of the wood grain, and this information is maintained as the board is cut and joined with other boards. (End-grain doesn't glue very well, and the software wants to be able to warn the user about such things.)
Finally, this code can take a solid model and generate the triangles needed for display purposes. It doesn't really care what display mechanism is used, but it only generates triangles with a right-handed coordinate system, as used by OpenGL and WPF 3D.
More on the WPF app
This app is not a shining example of good UI design, but it can do some cool things.
The woodworking project is represented as a series of steps, where each step is something like "cut a mortise in a board", or "join these two boards together". Those steps are shown in a list control in the upper left part of the window.
The main view in the center of the window shows the pretty picture produced from all those triangles. It supports rotation and zoom. By default, it shows the final result of the project, but if you want to see a specific step, you can choose from the list. When viewing a step that involves a join of two parts, it does an animation.
A woodworking plan can be parameterized with "variables", allowing dimensions to be changed and propagated accordingly. For example, the default plan shown is for a workbench, and one of the variables is called "Top Slab Length". If you change this value in the lower left part of the window and tap the Apply button, Sawdust will update the display.
.NET Core 3 might just get me interested in developing for the desktop again.
As I said at the top, if anybody is interested, the code is on GitHub. Enjoy!