Catel 7 Development Update
🚀 Catel 7 Development Update
Welcome to the Catel 7 development update! After nearly 15 years, Catel continues to evolve as a robust MVVM framework. While technologies like Silverlight, Windows Phone, and UWP have faded, Catel remains a powerful foundation for WPF applications, supported by a vibrant team and over 60 open-source components.
🛠️ Code Base Modernization
With .NET Core now the industry standard, it’s time to reimagine Catel’s future. Our goals:
- Adopt the modern .NET hosting model
- Switch to .NET logging
- Use .NET IoC/Service Provider
- Remove Catel’s complex serialization engine
To validate these changes, we built a proof of concept (PoC) to see how much responsibility could shift from Catel to .NET itself without losing what makes Catel so powerful.
⚡ Key Challenges & Solutions
During the proof of concept phase, we faced many challenges.
App Hosting Model
Supporting the .NET app hosting model required significant changes. We tested this by converting the Orchestra example application and its dependencies:
Catel.core, Catel.MVVM, Orc.Automation, Orc.Controls, Orc.FileSystem, Orc.Notifications, Orc.SystemInfo, Orc.Theming, Orchestra
It’s now required to register the services, making it easier to cherry-pick the libraries really needed for an application.
Example: Bootstrapping an Application
public App()
{
var hostBuilder = new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddCatelCore();
services.AddCatelMvvm();
services.AddOrcAutomation();
services.AddOrcControls();
services.AddOrcFileSystem();
services.AddOrcNotifications();
services.AddOrcSystemInfo();
services.AddOrcTheming();
services.AddOrchestraCore();
services.AddOrchestraShellRibbonFluent();
services.AddSingleton<IAboutInfoService, AboutInfoService>();
services.AddSingleton<IRibbonService, RibbonService>();
services.AddSingleton<IApplicationInitializationService, ApplicationInitializationService>();
services.AddSingleton<UserMessageCloseApplicationWatcher>();
services.AddLogging(x =>
{
x.AddConsole();
x.AddDebug();
});
services.AddSingleton<IChangelogProvider, Orchestra.Examples.Ribbon.Changelog.Providers.ChangelogProvider>();
});
_host = hostBuilder.Build();
IoCContainer.ServiceProvider = _host.Services;
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var serviceProvider = IoCContainer.ServiceProvider;
serviceProvider.CreateTypesThatMustBeConstructedAtStartup();
var shellService = serviceProvider.GetRequiredService<IShellService>();
shellService.CreateAsync<ShellWindow>();
}
Dependency injection
Dependency injection and XAML is hard. Although the main window can be constructed using the service provider, XAML requires a few things:
- Objects defined in XAML must have an empty constructor
- The service provider is not available to (all) xaml types
After trying different approaches, I think we came up with a great solution: source code generators. This allows any XAML type used with Catel to use dependency injection.
Example: View Constructor with DI
public partial class MyWindow
{
public MyWindow(ILogger<MyWindow>, IServiceProvider serviceProvider,
IWrapControlService wrapControlService, ILanguageService languageService)
: base(serviceProvider, wrapControlService, languageService)
{
InitializeComponent();
}
}
The source generator creates a default constructor for runtime:
public partial class MyWindow
{
private static T GetService<T>()
where T : class
{
if (Catel.CatelEnvironment.IsInDesignMode)
{
return null!;
}
return Catel.IoC.IoCContainer.ServiceProvider.GetRequiredService<T>();
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Catel.UserControlConstructors", "1.0.0.0")]
public MyWindow()
: this(GetService<ILogger<MyWindow>>(), GetService<IServiceProvider>(),
GetService<IWrapControlService>(), GetService<ILanguageService>())
{
}
}
You can even skip defining constructors, the source generators handle everything!
User controls without any constructors:
public partial class MyUserControl
{
private static T GetService<T>()
where T : class
{
if (Catel.CatelEnvironment.IsInDesignMode)
{
return null!;
}
return Catel.IoC.IoCContainer.ServiceProvider.GetRequiredService<T>();
}
partial void OnInitializingComponent();
partial void OnInitializedComponent();
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Catel.UserControlConstructors", "1.0.0.0")]
[ActivatorUtilitiesConstructor]
public MyUserControl(System.IServiceProvider serviceProvider, Catel.Services.IViewModelWrapperService viewModelWrapperService, Catel.MVVM.IDataContextSubscriptionService dataContextSubscriptionService)
: base(serviceProvider, viewModelWrapperService, dataContextSubscriptionService)
{
OnInitializingComponent();
InitializeComponent();
OnInitializedComponent();
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Catel.UserControlConstructors", "1.0.0.0")]
public MyUserControl()
: this(GetService<System.IServiceProvider>(), GetService<Catel.Services.IViewModelWrapperService>(), GetService<Catel.MVVM.IDataContextSubscriptionService>())
{
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Catel.UserControlConstructors", "1.0.0.0")]
public MyUserControl(Catel.MVVM.IViewModel? viewModel, System.IServiceProvider serviceProvider, Catel.Services.IViewModelWrapperService viewModelWrapperService, Catel.MVVM.IDataContextSubscriptionService dataContextSubscriptionService)
: base(viewModel, serviceProvider, viewModelWrapperService, dataContextSubscriptionService)
{
OnInitializingComponent();
InitializeComponent();
OnInitializedComponent();
}
}
Add custom logic in the partial OnInitializingComponent or OnInitializedComponent.
Dynamic type registration
Catel’s ServiceLocator allowed late-bound registration, but .NET requires all services to be registered up front. To keep initialization flexible, we introduced IConstructAtStartup:
- Implement the interface
- Register as singleton
- Call
serviceProvider.CreateTypesThatMustBeConstructedAtStartup();
Types are automatically instantiated and can run initialization code. Feedback welcome!
Logging
.NET logging uses DI, but static classes shouldn’t be forced to use DI just for logging. Our solution:
- Dependency injection: Inject
ILogger<T>(even for views) - Static logger: Use
LogManager.GetLogger(typeof(X))
LogManager detects the hosting model and provides the right logger instance, or a NullLogger for unit tests. It’s also possible to register a custom logger factory as fallback.
Simplified ViewModelBase
Catel’s ViewModelBase is powerful, but most view models don’t need all its features. With true DI, we’re splitting it:
- FeaturedViewModelBase: For advanced features (validation, throttling, etc.)
- ViewModelBase: Lightweight, for most use cases
💡 Decision: Go or No Go?
Modernizing Catel is possible! Now, we need your feedback on what matters most.
Pros:
- Standardized logging
- Standardized IoC/service registration
- Standardized app hosting model
- Lightweight ViewModelBase
Cons:
- Many breaking changes
- Removal of Catel IoC features (ServiceLocator, TypeFactory)
- Removal of Catel logging features
- Removal of Catel serialization features
We want your input! Share your thoughts in the Catel 7 migration ticket.