We have some large classes with dozens of properties with simple accessors that are basically identical:
public struct ObservableField<T>
{
internal T Val;
public ObservableFile(T val) => this.Val ) val;
...
}
public class DataConfiguration
{
private ObservableField<bool> isEnabled;
public bool IsEnabled
{
get => GetValue( ref isEnabled, nameof(IsEnabled));
set
{
if(SetValue( ref isEnabled, nameof(IsEnabled), value))
OnDataChanged(nameof(IsEnabled));
}
}
private ObservableField<WrapMode> dataWrapMode;
public bool DataWrapMode
{
get => GetValue( ref dataWrapMode, nameof(DataWrapMode));
set
{
if(SetValue( ref dataWrapMode, nameof(DataWrapMode), value))
OnDataChanged(nameof(DataWrapMode));
}
}
...
}
The reason why we want to change this is to make it more succinct and less error-prone as the code-base predates C#4. So no nameof
and there had been copy-paste errors:
private ObservableField<bool> dirty;
public bool Dirty
{
get => GetValue( ref dirty, "Dirty");
set
{
if(SetValue( ref dirty, "Dirty", value))
OnDataChanged(**"IsEnabled"**);
}
}
We are looking for a solution preferably with T4 code generation. Changing ObservableField
is not an option right now as it is pervasive throughout the larger codebase and there are some properties of that type that need different handling.
I also cant use a wrapper on that as there is a lot of magic with attributes and reflection going on to discover the properties as they are shown in a GUI.
with C-style macros it would be simple (but not pretty or idiomatic):
#define OBSERVABLE_WITH_ACCESSORS(name,type) \
private ObservableField<type> _##name; \
public ##type ##name \
{ \
get => GetValue( ref _##name, nameof(##name)); \
set \
{ \
if(SetValue( ref _##name, nameof(##name), value)) \
OnDataChanged(nameof(##name)); \
} \
}
How could this be done in preferably c#-8.0? if it can not be done easily in C#-8.0 suggestions for newer standards are also ok as we do intend to migrate in the future.
There's a bit you can do to reduce the boilerplate / copy-paste errors, without going to T4 / source generators:
CallerMemberNameAttribute
instead of hard-coding the nameOnDataChanged
callsI don't know how your GetValue
/ SetValue
are currently defined, but consider something like:
private T GetValue<T>(ref ObservableField<T> field, [CallerMemberName] string propertyName? = null)
{
// What your GetValue currently does, using propertyName
}
private void SetValue<T>(ref ObservableField<T> field, T value, [CallerMemberName] string propertyName? = null
{
if (/* What your SetValue currently does */)
OnDataChanged(propertyName);
}
Usage is then:
private ObservableField<bool> isEnabled;
public bool IsEnabled
{
get => GetValue(ref isEnabled);
set => SetValue(ref isEnabled, value);
}
For T4 generation, you'd need to be using reflection on the compiled version of the assembly, in order to generate code to be added to that assembly, which is a bit of a chicken-and-egg situation.
C#'s Source Generators would be a perfect fit, as they're able to generate code to inject into your assembly at compile-time, and they have access to the types in your assembly.
They're a bit of a steep learning curve, start here and here.
When they first came out, there were a lot of fairly simple examples for INotifyPropertyChanged
, generating properties given a class which just contained definitions of the backing fields (which is effectively what you're doing). However, do note that the old ISourceGenerator
interface has been deprecated in favour of IIncrementalGenerator
, and most of the old examples used ISourceGenerator
.