Vetuviem is a toolkit to support View to View Model binding (MVVM -> V2VM -> Ve-Tu-Viem) aimed at offering a structure to get more re-usability out of ReactiveUI.
- To give a mechanism to reduce the amount of boiler plate code being produced, by allowing some of the ReactiveUI specific logic to be hidden away
- Allow the developer to think along the lines of standard behaviours for controls by offering a way to produce re-usable behaviours through a class and\or function design pattern
- Allow the developer to focus on what matters on the ViewModel
- Reduce the cognitive load by
- Removing the risk of misusing 1 way or 2 way binding
- Remove the need for the user to think about having to cater for Bind vs BindCommand
- Offer a structure that allows for more work to be done potentially with Source Generators to reduce reflection and improve the build time developer experience.
This is currently a proof of concept alpha. For understanding of the design reasoning please see https://www.dpvreony.com/articles/designing-vetuviem/
| Purpose | Package | NuGet |
|---|---|---|
| Command Line Generation | Coming soon | Coming Soon |
| Visual Studio Integration | Vetuviem.SourceGenerator | |
| Core Functionality | Vetuviem.Core |
TODO
On CI runners you can occasionally find that the Source generators fail to load with soemthing along the lines of ErrorCode: ReferencesNewerCompiler, ReferencedCompilerVersion: 5.0.0.0.
NOTE: different .NET SDK or MSBuild versions are inconsistent with the error message, you may sometimes see it prefixed with ADR0001 or CS9057, other times it has no prefix.
This is currently (as of 2026-01) an issue with Github Actions where Visual Studio 2022 is on the Agent. This forces MSBuild to use the desired version rather than the one built into Visual Studio and MSBuild.
Place the following in a Dir.Build.Props file or the relevant C# project file(s).
<!--
This roslyn override is here because on the Windows agent the Visual Studio MSBuild gets invoked
so Vetuviem has been failing to generate because it needs a newer Roslyn compiler than the one installed in VS.
-->
<PropertyGroup>
<!-- Force use of .NET SDK Roslyn compiler on Windows when VS MSBuild is invoked -->
<UseSharedCompilation>false</UseSharedCompilation>
</PropertyGroup>
<ItemGroup>
<!-- Explicitly reference Roslyn 5.0+ compiler toolset -->
<PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="5.0.0" Condition="'$(MSBuildRuntimeType)' != 'Core'">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>Currently to write binding logic in the codebehind you have to write something similar to this for a single control
// Traditional ReactiveUI binding approach
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.Forename, v => v.Forename.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ForenameLengthRemaining, v => v.ForenameLengthRemaining.Content)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ForenameLengthRemaining, v => v.ForenameLengthRemaining.Foreground,
lengthRemaining => GetBrushForLengthRemaining(lengthRemaining))
.DisposeWith(disposables);
});But what if you have a way to simplify logic and offer a way to even make it reusable without all the boilerplate leg work?
// Vetuviem approach using ViewBindingModels
public sealed class QuestionnaireViewBindingModels : AbstractEnableViewToViewModelBindings<QuestionnaireView, QuestionnaireViewModel>
{
protected override IEnumerable<IControlBindingModel<QuestionnaireView, QuestionnaireViewModel>> GetBindings()
{
yield return new TextBoxControlBindingModel<QuestionnaireView, QuestionnaireViewModel>(vw => vw.Forename)
{
Text = new TwoWayBinding<QuestionnaireViewModel, string>(vm => vm.Forename),
};
yield return new LabelControlBindingModel<QuestionnaireView, QuestionnaireViewModel>(vw => vw.ForenameLengthRemaining)
{
Content = new OneWayBindingOnOneOrTwoWayBind<QuestionnaireViewModel, object>(vm => vm.ForenameLengthRemaining, o => o?.ToString() ?? string.Empty),
Foreground = new OneWayBindingWithConversionOnOneOrTwoWayBind<QuestionnaireViewModel, Brush, int>(vm => vm.ForenameLengthRemaining, lengthRemaining => GetBrushForLengthRemaining(lengthRemaining))
};
}
}Vetuviem source generators can be configured through MSBuild properties in your project file (.csproj). Add these properties to a <PropertyGroup> section to customize code generation behavior.
| Property | Description | Default Value |
|---|---|---|
Vetuviem_Root_Namespace |
Override the root namespace for generated code | (Project's root namespace) |
Vetuviem_Make_Classes_Public |
Make generated classes public instead of internal | false |
Vetuviem_Assemblies |
Comma-separated list of assemblies to scan for controls | (Platform-specific defaults) |
Vetuviem_Assembly_Mode |
How to use custom assemblies: Replace or Extend |
Replace |
Vetuviem_Base_Namespace |
Base namespace when using custom assemblies | (none) |
Vetuviem_Include_Obsolete_Items |
Include properties marked with ObsoleteAttribute |
false |
Vetuviem_Allow_Experimental_Properties |
Include properties marked with ExperimentalAttribute |
false |
Vetuviem_Logging_Implementation_Mode |
Logging implementation: None or SplatViaServiceLocator |
SplatViaServiceLocator |
Override the default root namespace for generated code. This is useful when you want generated binding models to reside in a specific namespace.
Example:
<PropertyGroup>
<Vetuviem_Root_Namespace>MyApp.Generated</Vetuviem_Root_Namespace>
</PropertyGroup>Controls the visibility of generated binding model classes. By default, classes are generated as internal. Set this to true to make them public.
Example:
<PropertyGroup>
<Vetuviem_Make_Classes_Public>true</Vetuviem_Make_Classes_Public>
</PropertyGroup>Specify a comma-separated list of assemblies to scan for control types. Use in conjunction with Vetuviem_Assembly_Mode to either replace or extend the default platform assemblies.
Example:
<PropertyGroup>
<Vetuviem_Assemblies>CustomControls.dll,ThirdPartyControls.dll</Vetuviem_Assemblies>
</PropertyGroup>Determines how the assemblies specified in Vetuviem_Assemblies are used:
Replace(default): Replace platform defaults with your custom assembliesExtend: Add custom assemblies in addition to platform defaults
Example:
<PropertyGroup>
<Vetuviem_Assembly_Mode>Extend</Vetuviem_Assembly_Mode>
</PropertyGroup>Used with custom assemblies to specify a base namespace. This allows third parties to use the generator and produce custom namespaces that inherit from the root or use a custom namespace.
Example:
<PropertyGroup>
<Vetuviem_Base_Namespace>MyCompany.Controls</Vetuviem_Base_Namespace>
</PropertyGroup>Controls whether properties marked with ObsoleteAttribute are included in code generation. By default, obsolete items are excluded.
Example:
<PropertyGroup>
<Vetuviem_Include_Obsolete_Items>true</Vetuviem_Include_Obsolete_Items>
</PropertyGroup>Starting with .NET 10, properties can be marked with ExperimentalAttribute to indicate they are experimental APIs. This property controls how these are handled:
When false or not set (default):
- Properties marked with
ExperimentalAttributeare excluded from code generation - No warnings are generated
- Experimental properties are silently skipped
- Ensures backward compatibility
When true:
- Properties marked with
ExperimentalAttributeare included in code generation - A
SuppressMessageattribute is automatically added to suppress experimental warnings - The diagnostic ID from the
ExperimentalAttributeis extracted and used in the suppression
Example:
<PropertyGroup>
<Vetuviem_Allow_Experimental_Properties>true</Vetuviem_Allow_Experimental_Properties>
</PropertyGroup>Example Control Property:
[Experimental("WFDEV001")]
public ThemeMode ThemeMode { get; set; }Generated Binding Property (when enabled):
[SuppressMessage("Usage", "WFDEV001")]
public IOneOrTwoWayBind<TViewModel, ThemeMode>? ThemeMode { get; init; }Notes:
- The diagnostic ID is read from the first constructor argument of
ExperimentalAttribute - Only properties with
publicaccessibility are considered for generation (standard behavior)
Controls the logging implementation generated in binding code:
None: No logging implementationSplatViaServiceLocator(default): Uses Splat logging via service locator
Example:
<PropertyGroup>
<Vetuviem_Logging_Implementation_Mode>None</Vetuviem_Logging_Implementation_Mode>
</PropertyGroup>For support, please:
- Check the design article for understanding the design reasoning
- Report bugs or request features via GitHub Issues
- Ask questions in GitHub Discussions
- Review the sample applications in the
srcdirectory for practical examples
Contributions are welcome! Please:
- Read the Code of Conduct
- Fork the repository and create a feature branch
- Write tests for your changes
- Ensure all tests pass and code follows the existing style
- Submit a Pull Request with a clear description of your changes
This project is licensed under the MIT License - see the LICENSE file for details.