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)