Skip to content

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:

Reflection Powered Settings

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
}