diff --git a/IPA.Loader/Config/Stores/Attributes.cs b/IPA.Loader/Config/Stores/Attributes.cs
index 7cc2ee39..87378e93 100644
--- a/IPA.Loader/Config/Stores/Attributes.cs
+++ b/IPA.Loader/Config/Stores/Attributes.cs
@@ -1,11 +1,19 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IPA.Config.Stores.Attributes
{
+ ///
+ /// Indicates that the generated subclass of the attribute's target should implement .
+ /// If the type this is applied to already inherits it, this is implied.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public sealed class NotifyPropertyChangesAttribute : Attribute { }
+
///
/// Causes a field or property in an object being wrapped by to be
/// ignored during serialization and deserialization.
diff --git a/IPA.Loader/Config/Stores/GeneratedStore.cs b/IPA.Loader/Config/Stores/GeneratedStore.cs
index 4c261989..1b87df5e 100644
--- a/IPA.Loader/Config/Stores/GeneratedStore.cs
+++ b/IPA.Loader/Config/Stores/GeneratedStore.cs
@@ -15,11 +15,12 @@ using System.IO;
using Boolean = IPA.Config.Data.Boolean;
using System.Collections;
using IPA.Utilities;
+using System.ComponentModel;
#if NET3
using Net3_Proxy;
using Array = Net3_Proxy.Array;
#endif
-
+
[assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedExtension.AssemblyVisibilityTarget)]
namespace IPA.Config.Stores
@@ -84,6 +85,11 @@ namespace IPA.Config.Stores
/// called after Changed() is called, but before the write lock is released.
/// Unless you have a very good reason to use the nested , avoid it.
///
+ ///
+ /// If is marked with , the resulting object will implement
+ /// . Similarly, if implements ,
+ /// the resulting object will implement it and notify it too.
+ ///
///
/// the type to wrap
/// the to register to
@@ -121,6 +127,10 @@ namespace IPA.Config.Stores
{
void CopyFrom(T source, bool useLock);
}
+ internal interface IGeneratedPropertyChanged : INotifyPropertyChanged
+ {
+ PropertyChangedEventHandler PropertyChangedEvent { get; }
+ }
internal class Impl : IConfigStore
{
@@ -175,7 +185,7 @@ namespace IPA.Config.Stores
internal static MethodInfo ImplChangeTransactionMethod = typeof(Impl).GetMethod(nameof(ImplChangeTransaction));
public static IDisposable ImplChangeTransaction(IGeneratedStore s, IDisposable nest) => FindImpl(s).ChangeTransaction(nest);
// TODO: improve trasactionals so they don't always save in every case
- public IDisposable ChangeTransaction(IDisposable nest, bool takeWrite = true)
+ public IDisposable ChangeTransaction(IDisposable nest, bool takeWrite = true)
=> GetFreeTransaction().InitWith(this, !inChangeTransaction, nest, takeWrite && !WriteSyncObject.IsWriteLockHeld);
private ChangeTransactionObj GetFreeTransaction()
@@ -374,6 +384,7 @@ namespace IPA.Config.Stores
#region Parse base object structure
const BindingFlags overrideMemberFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
+
var baseChanged = type.GetMethod("Changed", overrideMemberFlags, null, Type.EmptyTypes, Array.Empty());
if (baseChanged != null && IsMethodInvalid(baseChanged, typeof(void))) baseChanged = null;
@@ -386,6 +397,9 @@ namespace IPA.Config.Stores
var baseChangeTransaction = type.GetMethod("ChangeTransaction", overrideMemberFlags, null, Type.EmptyTypes, Array.Empty());
if (baseChangeTransaction != null && IsMethodInvalid(baseChangeTransaction, typeof(IDisposable))) baseChangeTransaction = null;
+ var isINotifyPropertyChanged = type.FindInterfaces((i, t) => i == (Type)t, typeof(INotifyPropertyChanged)).Length != 0;
+ var hasNotifyAttribute = type.GetCustomAttribute() != null;
+
var structure = new List();
bool ProcessAttributesFor(ref SerializedMemberInfo member)
@@ -597,6 +611,135 @@ namespace IPA.Config.Stores
const MethodAttributes propertyMethodAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
const MethodAttributes virtualPropertyMethodAttr = propertyMethodAttr | MethodAttributes.Virtual | MethodAttributes.Final;
const MethodAttributes virtualMemberMethod = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.Final;
+
+ #region INotifyPropertyChanged
+ MethodBuilder notifyChanged = null;
+ if (isINotifyPropertyChanged || hasNotifyAttribute)
+ {
+ var INotifyPropertyChanged_t = typeof(INotifyPropertyChanged);
+ typeBuilder.AddInterfaceImplementation(INotifyPropertyChanged_t);
+
+ var INotifyPropertyChanged_PropertyChanged =
+ INotifyPropertyChanged_t.GetEvent(nameof(INotifyPropertyChanged.PropertyChanged));
+
+ var PropertyChangedEventHandler_t = typeof(PropertyChangedEventHandler);
+ var PropertyChangedEventHander_Invoke = PropertyChangedEventHandler_t.GetMethod(nameof(PropertyChangedEventHandler.Invoke));
+
+ var PropertyChangedEventArgs_t = typeof(PropertyChangedEventArgs);
+ var PropertyChangedEventArgs_ctor = PropertyChangedEventArgs_t.GetConstructor(new[] { typeof(string) });
+
+ var Delegate_t = typeof(Delegate);
+ var Delegate_Combine = Delegate_t.GetMethod(nameof(Delegate.Combine), BindingFlags.Static | BindingFlags.Public, null,
+ new[] { Delegate_t, Delegate_t }, Array.Empty());
+ var Delegate_Remove = Delegate_t.GetMethod(nameof(Delegate.Remove), BindingFlags.Static | BindingFlags.Public, null,
+ new[] { Delegate_t, Delegate_t }, Array.Empty());
+
+ var CompareExchange = typeof(Interlocked).GetMethods()
+ .Where(m => m.Name == nameof(Interlocked.CompareExchange))
+ .Where(m => m.ContainsGenericParameters)
+ .Where(m => m.GetParameters().Length == 3).First()
+ .MakeGenericMethod(PropertyChangedEventHandler_t);
+
+ var PropertyChanged_backing = typeBuilder.DefineField("PropertyChanged", PropertyChangedEventHandler_t, FieldAttributes.Private);
+
+ var add_PropertyChanged = typeBuilder.DefineMethod("PropertyChanged",
+ MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.Virtual,
+ null, new[] { PropertyChangedEventHandler_t });
+ typeBuilder.DefineMethodOverride(add_PropertyChanged, INotifyPropertyChanged_PropertyChanged.GetAddMethod());
+
+ {
+ var il = add_PropertyChanged.GetILGenerator();
+
+ var loopLabel = il.DefineLabel();
+ var delTemp = il.DeclareLocal(PropertyChangedEventHandler_t);
+
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldfld, PropertyChanged_backing);
+
+ il.MarkLabel(loopLabel);
+ il.Emit(OpCodes.Stloc, delTemp);
+
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldflda, PropertyChanged_backing);
+
+ il.Emit(OpCodes.Ldloc, delTemp);
+ il.Emit(OpCodes.Ldarg_1);
+ il.Emit(OpCodes.Call, Delegate_Combine);
+ il.Emit(OpCodes.Castclass, PropertyChangedEventHandler_t);
+
+ il.Emit(OpCodes.Ldloc, delTemp);
+ il.Emit(OpCodes.Call, CompareExchange);
+
+ il.Emit(OpCodes.Dup);
+ il.Emit(OpCodes.Ldloc, delTemp);
+ il.Emit(OpCodes.Bne_Un_S, loopLabel);
+
+ il.Emit(OpCodes.Ret);
+ }
+
+ var remove_PropertyChanged = typeBuilder.DefineMethod("PropertyChanged",
+ MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.Virtual,
+ null, new[] { PropertyChangedEventHandler_t });
+ typeBuilder.DefineMethodOverride(remove_PropertyChanged, INotifyPropertyChanged_PropertyChanged.GetRemoveMethod());
+
+ {
+ var il = remove_PropertyChanged.GetILGenerator();
+
+ var loopLabel = il.DefineLabel();
+ var delTemp = il.DeclareLocal(PropertyChangedEventHandler_t);
+
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldfld, PropertyChanged_backing);
+
+ il.MarkLabel(loopLabel);
+ il.Emit(OpCodes.Stloc, delTemp);
+
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldflda, PropertyChanged_backing);
+
+ il.Emit(OpCodes.Ldloc, delTemp);
+ il.Emit(OpCodes.Ldarg_1);
+ il.Emit(OpCodes.Call, Delegate_Remove);
+ il.Emit(OpCodes.Castclass, PropertyChangedEventHandler_t);
+
+ il.Emit(OpCodes.Ldloc, delTemp);
+ il.Emit(OpCodes.Call, CompareExchange);
+
+ il.Emit(OpCodes.Dup);
+ il.Emit(OpCodes.Ldloc, delTemp);
+ il.Emit(OpCodes.Bne_Un_S, loopLabel);
+
+ il.Emit(OpCodes.Ret);
+ }
+
+ var PropertyChanged_event = typeBuilder.DefineEvent(nameof(INotifyPropertyChanged.PropertyChanged), EventAttributes.None, PropertyChangedEventHandler_t);
+ PropertyChanged_event.SetAddOnMethod(add_PropertyChanged);
+ PropertyChanged_event.SetRemoveOnMethod(remove_PropertyChanged);
+
+ notifyChanged = typeBuilder.DefineMethod("<>NotifyChanged",
+ MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Final, null, new[] { typeof(string) });
+
+ {
+ var il = notifyChanged.GetILGenerator();
+
+ var invokeNonNull = il.DefineLabel();
+
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldfld, PropertyChanged_backing);
+ il.Emit(OpCodes.Dup);
+ il.Emit(OpCodes.Brtrue, invokeNonNull);
+ il.Emit(OpCodes.Pop);
+ il.Emit(OpCodes.Ret);
+
+ il.MarkLabel(invokeNonNull);
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldarg_1);
+ il.Emit(OpCodes.Newobj, PropertyChangedEventArgs_ctor);
+ il.Emit(OpCodes.Call, PropertyChangedEventHander_Invoke);
+ il.Emit(OpCodes.Ret);
+ }
+ }
+ #endregion
#region IGeneratedStore
typeBuilder.AddInterfaceImplementation(typeof(IGeneratedStore));
@@ -765,8 +908,18 @@ namespace IPA.Config.Stores
EmitDeserializeMember(il, member, nextLabel, il => il.Emit(OpCodes.Ldloc_S, valueLocal), GetLocal);
}
- il.MarkLabel(nextLabel);
-
+ il.MarkLabel(nextLabel);
+
+ if (notifyChanged != null)
+ {
+ foreach (var member in structure)
+ {
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldstr, member.Name);
+ il.Emit(OpCodes.Call, notifyChanged);
+ }
+ }
+
il.Emit(OpCodes.Ret);
}
#endregion
@@ -935,6 +1088,16 @@ namespace IPA.Config.Stores
il.EndExceptionBlock();
}
+ if (notifyChanged != null)
+ {
+ foreach (var member in structure)
+ {
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldstr, member.Name);
+ il.Emit(OpCodes.Call, notifyChanged);
+ }
+ }
+
var endLock = il.DefineLabel();
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Brfalse, endLock);
@@ -975,7 +1138,7 @@ namespace IPA.Config.Stores
il.Emit(OpCodes.Ret);
}
}
- #endregion
+ #endregion
#region Members
foreach (var member in structure.Where(m => m.IsVirtual))
@@ -1042,6 +1205,12 @@ namespace IPA.Config.Stores
il.EndExceptionBlock();
+ if (notifyChanged != null)
+ {
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldstr, member.Name);
+ il.Emit(OpCodes.Call, notifyChanged);
+ }
il.Emit(OpCodes.Ret);
}
@@ -1056,28 +1225,9 @@ namespace IPA.Config.Stores
).Compile();
return (creatorDel, genType);
- }
-
- private delegate LocalBuilder GetLocal(Type type, int idx = 0);
-
- private static GetLocal MakeGetLocal(ILGenerator il)
- { // TODO: improve this shit a bit so that i can release a hold of a variable and do more auto managing
- var locals = new List();
-
- LocalBuilder GetLocal(Type ty, int i = 0)
- {
- var builder = locals.Where(b => b.LocalType == ty).Skip(i).FirstOrDefault();
- if (builder == null)
- {
- builder = il.DeclareLocal(ty);
- locals.Add(builder);
- }
- return builder;
- }
-
- return GetLocal;
- }
-
+ }
+
+ #region Logs
private static readonly MethodInfo LogErrorMethod = typeof(GeneratedStore).GetMethod(nameof(LogError), BindingFlags.NonPublic | BindingFlags.Static);
internal static void LogError(Type expected, Type found, string message)
{
@@ -1092,9 +1242,10 @@ namespace IPA.Config.Stores
internal static void LogWarningException(Exception exception)
{
Logger.config.Warn(exception);
- }
-
-
+ }
+ #endregion
+
+ #region Correction
private static bool NeedsCorrection(SerializedMemberInfo member)
{
var expectType = GetExpectedValueTypeForType(member.IsNullable ? member.NullableWrappedType : member.Type);
@@ -1155,9 +1306,31 @@ namespace IPA.Config.Stores
il.Emit(OpCodes.Newobj, member.Nullable_Construct);
il.MarkLabel(endLabel);
+ }
+ #endregion
+
+ #region Utility
+
+ private delegate LocalBuilder GetLocal(Type type, int idx = 0);
+
+ private static GetLocal MakeGetLocal(ILGenerator il)
+ { // TODO: improve this shit a bit so that i can release a hold of a variable and do more auto managing
+ var locals = new List();
+
+ LocalBuilder GetLocal(Type ty, int i = 0)
+ {
+ var builder = locals.Where(b => b.LocalType == ty).Skip(i).FirstOrDefault();
+ if (builder == null)
+ {
+ builder = il.DeclareLocal(ty);
+ locals.Add(builder);
+ }
+ return builder;
+ }
+
+ return GetLocal;
}
- #region Utility
private static void EmitLoad(ILGenerator il, SerializedMemberInfo member, Action thisarg = null)
{
if (thisarg == null)