Reflection Powered Settings
This is a control that is able to attach to any DTO (Data Transfer Object) style class and generate a UI for each of the fields. This is very good for getting a basic settings editor up and running with minimal WPF experience.
Originally it was part of Synthesis to provide UI controls for patcher settings, but has been moved up to Mutagen's libraries for more general use.
Overview
Let's take an example DTO class
public class TestSettings
{
public bool MyBool;
public string MyString = string.Empty;
public FormKey MyFormKey;
public IFormLinkGetter<IArmorGetter> MyArmor = FormLink<IArmorGetter>.Null;
}
We can supply that definition to this control, and get a UI immediately:
As such, it is an easy way to get a decent UI for any class, and is very helpful if:
- You aren't used to WPF and just want to get something up and running
- If you don't know ahead of time what fields will exist (Synthesis patchers being a prime example)
Nesting
The reflection systems allow nested classes, and utilize them to create "sections".
Take this modified setup for example:
public class TestSettings
{
public bool MyBool;
public string MyString = string.Empty;
public FormKey MyFormKey;
public IFormLinkGetter<IArmorGetter> MyArmor = FormLink<IArmorGetter>.Null;
public SubSettings SubSetting = new();
}
public class TopLevelSettings
{
public TestSettings SectionOne = new();
public TestSettings SectionTwo = new();
}
public class SubSettings
{
public bool SubSetting;
}
This would yield the following display:
Attributes
Since the internal systems are constructing a view for you based on the contents of your class, you have reduced control over what is displayed, where, and how.
The system provides some Attributes to help give back some of this control. They all live within the namespace Mutagen.Bethesda.WPF.Reflection.Attributes
NOTE: The naming of these attributes differs slightly from the names Synthesis uses for its autogenerated setting systems
Ignore
A marker attribute that will make a specific field not display
[Ignore]
public bool MyIgnoredBool { get; set; }
Tooltip
Sets the tooltip to display
[Tooltip("I am a setting with a lot of detail to consider")
public bool SomeComplexSetting { get; set; }
SettingName
Will set explicitly what text to display as the name for the field
[SettingName("Please Display This Text")
public bool SomeDerpyName { get; set; }
JsonDiskName
An attribute to change what text key is used when persisting/loading from json
[JsonDiskName("some-setting")
public bool SomeSetting { get; set; }
MaintainOrder
Unfortunately, C# cannot properly guarantee order of fields when utilizing reflection. This attribute is a marker that helps maintain desired ordering.
[MaintainOrder]
public bool FirstSetting { get; set; }
[MaintainOrder]
public bool SecondSetting { get; set; }
ObjectNameMember
This attribute applies to the settings class itself, and defines the object type naming to show when nested classes are involved.
[ObjectNameMember(nameof(TestSettings.MyString))]
public class TestSettings
{
// Will now drive the text displayed when in a nested scenario
public string MyString = string.Empty;
}
Now MyString
drives the display in the top navigation bar
FormLinkPickerCustomization
Allows you to list explicitly the types that should be allowed in the picker. It loses generic type safety, so only preferable if there is not already a LinkInterface or AspectInterface that describes the several types you want to target.
[FormLinkPickerCustomization(typeof(IArmorGetter), typeof(IWeaponGetter))]
public IFormLinkGetter ArmorsAndWeapons { get; set; } = FormLinkInformation.Null;
Allowed Field Types
bool
string
- Integers (signed and unsigned)
float
double
decimal
ModKey
FormKey
- FormLinks
- Other classes with appropriate fields
- Lists/Arrays/HashSets/IEnumerables (of the above types)
- Dictionaries, in certain scenarios:
- Key is an enum
- Key is a
string
Implementing Within a UI Yourself
Note that Synthesis users do not need to worry about any of this, as it is handled for them. But if you're making a UI of your own, and want to lean on the autogenerated setting systems, this would be how to do it.
View Side
<UserControl
...
xmlns:refl="clr-namespace:Mutagen.Bethesda.WPF.Reflection;assembly=Mutagen.Bethesda.WPF" >
<refl:AutogeneratedSettingView DataContext="{Binding MyReflectionViewModel}" />
</UserControl>
ViewModel Side
// Some mechanics shown here are from `ReactiveUI`, or `Noggog.WPF`
public class MyViewModel : ViewModel
{
public ReflectionSettingsVM MyReflectionViewModel { get; }
public MyViewModel()
{
// Create a GameEnvironment
var env = GameEnvironment.Typical.Skyrim(
SkyrimRelease.SkyrimSE,
// By passing in this preference, we only cache FormKey/EditorID info, keeping memory usage down
LinkCachePreferences.OnlyIdentifiers());
// When the ViewModel is destroyed, clean up the environment object. Good practice
env.DisposeWith(this);
// Create a prototype instance of the class we want it to display
var defaultSettings = new TestSettings()
{
// Can specify some default settings we want to appear
MyBool = true
};
MyReflectionViewModel = new ReflectionSettingsVM(
ReflectionSettingsParameters.CreateFrom(
// Give it the class we want to display
defaultSettings,
// Can give it a live load order, or just some raw listings
env.LoadOrder.Select(x => new ModListingVM(x.Value, env.DataFolderPath.Path)),
// Give it the link cache, for lookups if needed
env.LinkCache));
// Reflection display systems will now take over and display the appropriate fields
}
// Function save the current state of the reflection display to a json file
public void SaveToDisk(string somePath)
{
var jsonObj = new JObject();
MyReflectionViewModel.ObjVM.Persist(jsonObj);
File.WriteAllText(somePath, jsonObj.ToString());
}
// More functionality to access data from the reflection view will be added
}