Today, I’ll explain the project structure I use when building WinUI apps.
This article is part of a series about creating a simple media player using WinUI 3 framework. Other articles in this series can be found under the MusicApp tag.
Before we begin, I should note that these are purely my personal preferences — not the ultimate truth. I just find this approach convenient and comfortable for my workflow.
In desktop apps, I always separate business logic from UI implementation. This way, you can swap UI frameworks with minimal changes. View models, models, service interfaces, and business logic commands all go into a separate project with the .Core
suffix. The .Core
project has no dependency on Windows App SDK and could theoretically work with any UI framework (like Avalonia or WPF). All views and platform-specific services remain in the main project.
Sometimes certain functionality logically belongs in the .Core
project but depends heavily on platform-specific services. There are two approaches to solve this.
The simpler solution: Extract an interface from the service that can be consumed by the .Core
project. For example, an IFileService
interface that abstracts file operations:
/// <summary>
/// Provides abstraction for platform-specific file operations
/// </summary>
public interface IFileService
{
/// <summary>
/// Checks if the specified file format is supported
/// </summary>
/// <param name="fileName">The file name with extension to check</param>
/// <returns>True if the file format is supported, otherwise false</returns>
bool IsSupported(string? fileName);
/// <summary>
/// Opens a file picker dialog to select multiple files
/// </summary>
/// <returns>A list of selected file paths</returns>
Task<IList<string>> PickMultipleFilesAsync();
/// <summary>
/// Loads media items from specified file paths
/// </summary>
/// <param name="fileNames">Collection of file paths to load</param>
/// <returns>A list of loaded media items</returns>
Task<IList<MediaItem>> LoadMediaItems(IEnumerable<string> fileNames);
}
The MediaItem
s loading implementation depends on Windows App SDK types, which violates the .Core
project’s platform-agnostic requirements.
A more interesting case occurs when functionality logically belongs in the model but depends on platform-specific implementation. For example, take the MediaItem
class that stores metadata. It would make sense for it to also hold the album cover. But images consume memory, and we only need one cover at a time!
The logical solution? Load covers on demand. Since this involves platform-dependent code, we use an extension method:
// Implemented in the main project (we never use covers outside UI, so this is justified)
public static async Task<Image> LoadCover(this MediaItem item)
{
/* ... */
}
This keeps the core model clean while handling platform-specific needs only when required.
Next time, we’ll start building the UI.