johnnys.newsjohnnys.news

by Joachim Leonfellner ✌️ i'm an indie dev in ❤️ with side-projects

Mar 16, 20221949 words in 13 min


OpenSee 👀 - an OpenSea downloader built with .NET MAUI

UPDATE

After various requests to publish the app i finally found the time to prepare a CI/CD pipeline for OpenSee. The workflow builds the macOS and Windows version and generates a new GitHub release. So no need to build the source from scratch - you can just download the executables now.

I also decided to share the project on Product Hunt so if you find OpenSee helpful you can leave a 👍 and comments there.

OpenSee 👀

Since i’m excited about the procedural generation of digital art i recently started investigating big collections on the OpenSea NFT marketplace and how the images compare and differ. For my tests i needed a big collection of images to further process them, that’s why i set out to build a simple OpenSea downloader. I’m again experimenting with the latest .NET MAUI previews and decided to check it’s capabilities for developing desktop apps.

TL;DR

OpenSee can download collections from OpenSea without requiring an API-Key or some sort of login. Here it is in action:

OpenSee 👀

it is a WinUI application that can download NFT associated assets from the OpenSea website. Since most NFTs there are not storing the asset data on-chain, we can just scrape the urls from the website and download all the data. This is exactly what i’m doing with OpenSee. Basically it spins up a headless browser, navigates to the url and looks for image tags… easy 😅

And yes, i know that there is more to the technology of NFTs than just the possession of jpg files - my intention with OpenSee is not downplay the NFT idea. For me it’s just a simple project for fun.

I’m using playwright-dotnet under the hood, which allows me to programmatically interact with the OpenSea website via a headless browser. Because of this no API-Key or other type of login is required to use OpenSee. In the background it just navigates the website like a human visitor would do. Using this approach of course makes scraping of very big collections a bit time-consuming, but i just leave it running in the background and eat RAM all the time.

I started with a blank dotnet new maui app and removed the iOS and Android platforms because playwright-dotnet is not supported in those environments. I may build a macOS desktop solution as well, but it probably has to be a .NET6 native app since i don’t think i can use playwright in a maccatalyst sandbox.

One important note about my code. I tried to build something useful as quickly as possible and not focus too much on a perfect architecture or many abstraction layers within the code. So don’t be surprised if you can’t find ViewModels in my spaghetti code - code-behind FTW 🥳. If you can handle some ugly 🍝 code - here is the repository:

https://github.com/nor0x/OpenSee

I have decided to also release a macOS version of OpenSee. Therefore i have updated the repository with a common project that contains platform agnostic code and ViewModels which are shared accross Windows and macOS.

the logic is currently not optimized at all, so if you have feedback please let me know!
I have improved the logic a bit, but i’m still happy if you have feedback 😘

.NET MAUI for Windows

coming from Xamarin.Forms i was a bit skeptical about using .NET MAUI to build a Windows App. That’s because Xamarin.Forms is using UWP for Windows which itself puts quite some (unnecessary) obstacles in the front of developers. In addition to the architectural flaws of UWP the Xamarin.Forms wrapper around the APIs are not in a good state and were always a second-class citizen in Xamarin.Forms.

But luckily that changed with .NET MAUI, under the hood the Windows App SDK (Project Reunion) is used to build apps for Windows. This means we get good-looking UI controls, access to Win32 APIs and we don’t need some weird hacks to bypass an app-sandbox anymore.

Compared to Xamarin.Forms the developer story for Windows apps is much better with .NET MAUI, but it’s still not perfect. If we look at the project file of a blank dotnet new maui app, we could still get the feeling that the Windows target is still not on par with iOS and Android. Too many Windows specific parts that ideally should be abstracted.

Let’s take a look at the TargetFrameworks for example, we see that there is a windows specific check which defines if dotnet or msbuild is used to build the WinUI app.

1
2
3
<PropertyGroup>
<TargetFrameworks>net6.0-android;net6.0-ios;net6.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows')) and '$(MSBuildRuntimeType)' == 'Full'">$(TargetFrameworks);net6.0-windows10.0.19041</TargetFrameworks>

it seems that the .NET MAUI team is aware of this and the limitation is currently one level deeper in WinUI3. Of course this is nothing that limits your development directly. But i think the goal for MAUI really should be to unify development for all platforms to be the same - also the tooling.

While i am writing this blog post, Preview 14 of .NET MAUI was released so i have updated OpenSee to benefit from the many bugfixes. This release also has some nice additions specifically for desktop scenarios 👏

Still i ran into some issues while developing OpenSee, most of them are design limitations either on the MAUI or WinUI level.

rounded corners

this deserves and extra headline, because what ideally should be a one-liner, bugged me quite a bit. i wanted the app to have rounded corners for the main content container. I know the API so i knew there are multiple ways to implement it.

I started by wrapping my whole content in a Frame layout which has a CornerRadius property to clip its content. I wasn’t successful with this approach because i ran into the issue that the frame breaks some child controls and doesn’t resize correctly. I have created according issues here and here.

I decided to not use a Frame but rather clip the main Grid layout with a RoundRectangleGeometry shape. The implementation looks something like this

1
2
3
4
5
6
<Grid>
<Grid.Clip>
<RoundRectangleGeomtry CornerRadius="12">
<Grid.Clip>
<!-- content -->
</Grid>

this seems like a much cleaner approach, but unfortunately is also broken and leads to an instant crash of your app. There is a pull-request for this issue which was merged just a few hours before Preview 14 was announced but unfortunately didn’t make it into the latest bit. It’s still very positive to see how quick (especially Windows related) issues are addressed in .NET MAUI - compared to Xamarin.Forms. Since .NET MAUI and also most of it’s CI/CD is open-source i decided to just use the nightly artifacts which you can get directly from their Azure DevOps project https://dev.azure.com/xamarin/public/_build?definitionId=57.

entry placeholders

another visual glitch that bugged me was placeholder alignment in the Entry control. There is no property on the .NET MAUI level which abstracts this - which is fine maybe not all platforms offer a similar functionality. Luckily it’s very easy to get access to the native control via the handler architecture in MAUI. On Windows the Entry is using a WinUI TextBox - which i can access via this code:

1
2
3
4
5
6
7
Microsoft.Maui.Handlers.EntryHandler.EntryMapper.AppendToMapping(nameof(Entry), (h, v) =>
{
if (h.NativeView is Microsoft.UI.Xaml.Controls.TextBox tb)
{
tb.VerticalContentAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center;
}
});

I set the VerticalContentAlignment property toVerticalAlignment.Center which didn’t change anything, and according to a GitHub issue from September 2020 - it’s not that straight forward to just vertically center the placeholder in a WinUI TextBox. So if you read this and you have more info about it please let me know - i’m not really deep in the WinUI API but i’m happy to learn more about it.

Window Sizing

Another thing i wanted to implement was a fixed Window size and location for the app. There is no official documentation in .NET MAUI about this, but the following approach was presented in multiple issues and discussion posts:

1
2
3
4
5
6
7
8
9
10
11
12
13
    Microsoft.Maui.Handlers.WindowHandler.WindowMapper[nameof(IWindow)] = (handler, view) =>
{
#if WINDOWS
var nativeWindow = handler.NativeView;
nativeWindow.Activate();
IntPtr windowHandle = PInvoke.User32.GetActiveWindow();

PInvoke.User32.SetWindowPos(windowHandle,
PInvoke.User32.SpecialWindowHandles.HWND_TOP,
0, 0, width, height, // width and height are ints
PInvoke.User32.SetWindowPosFlags.SWP_NOMOVE);
#endif
};

This code is using the Win32 window APIs and works fine, but i thought there must be a more modern way to control a Window on Windows. I did some research and found the AppWindow API in WinUI which has everything i needed.

1
2
3
4
5
6
7
8
9
10
11
12
    Microsoft.Maui.Handlers.WindowHandler.WindowMapper.AppendToMapping(nameof(IWindow), (handler, view) =>
{
#if WINDOWS
var nativeWindow = handler.NativeView;
nativeWindow.Activate();

IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow);
WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd);
AppWindow appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);
appWindow.MoveAndResize(new Windows.Graphics.RectInt32(0, 0, 500, 500));
#endif
});

I can use the WindowHandler to get the native window and via a WindowId i get an AppWindow object which has all the methods for resizing and movement that i needed. Still not a super-clean API but better than Win32 in my opinion.

Bonus 🖼️🖼️🖼️

I wanted to show a bunch of images downloaded with OpenSee in a matrix. I tried something via Photoshop automation and it’s now running for half an hour without any result and it’s just eating memory like crazy.

I decided to create a script that reads a directory for images and generates a matrix image with the content.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#r "nuget: SkiaSharp, 2.88.0-preview.232"

using SkiaSharp;

var files = System.IO.Directory.GetFiles("C:/imgs");

var bmp = new SKBitmap(3000,3000);
var result = new SKCanvas(bmp);
int rows = 30;
int cols = 30;

for(int i = 0; i < rows; i++)
{
for(int j = 0; j < cols; j++)
{
var file = files[i * cols + j];
var bitmap = SKBitmap.Decode(file);
result.DrawBitmap(bitmap, new SKRect(j * bitmap.Width, i * bitmap.Height, (j + 1) * bitmap.Width, (i + 1) * bitmap.Height));
}
}

var data = bmp.Encode(SKEncodedImageFormat.Jpeg, 100);
System.IO.File.WriteAllBytes("C:/imgs/result.jpg", data.ToArray());

The C# notebook reads an images folder iterates over the files and uses SkiaSharp to create a canvas with a blank bitmap. It then draws the bitmap in rows and columns and saves the resulting bitmap as a file. It ran for ~0.3 seconds 🥳 and produced the following file

For the moment that’s all for OpenSee, keep an 👀 on the repository. For now it’s in a state that serves me well but i may add some cleanup and more stuff to it soon.