diff --git a/src/net/Qml.Net.Tests/Qml/AutoSignalTests.cs b/src/net/Qml.Net.Tests/Qml/AutoSignalTests.cs new file mode 100644 index 00000000..a434c73f --- /dev/null +++ b/src/net/Qml.Net.Tests/Qml/AutoSignalTests.cs @@ -0,0 +1,68 @@ +using FluentAssertions; +using Xunit; + +namespace Qml.Net.Tests.Qml +{ + public class SomeTestClass + { + public string PropertyWithoutSignal { get; set; } + + [NotifySignal] public string PropertyWithNotifySignal { get; set; } + + [NotifySignal("myCustomNotifySignal")] public string PropertyWithCustomNotifySignal { get; set; } + + public void TriggerDefaultSignal() + { + this.ActivateSignal("dynamic__PropertyWithoutSignalChanged"); + } + + public bool DefaultSignalReceived { get; set; } = false; + + public void TriggerNotifySignal() + { + this.ActivateNotifySignal(nameof(PropertyWithNotifySignal)); + } + + public bool NotifySignalReceived { get; set; } = false; + + public void TriggerCustomNotifySignal() + { + this.ActivateNotifySignal(nameof(PropertyWithCustomNotifySignal)); + } + + public bool CustomNotifySignalReceived { get; set; } = false; + } + + public class AutoSignalTests : BaseQmlTestsWithInstance + { + public AutoSignalTests() + : base(true) + { + } + + [Fact] + public void Does_register_autoSignals() + { + RunQmlTest( + "testClass", + @" + testClass.dynamic__PropertyWithoutSignalChanged.connect(function() { + testClass.defaultSignalReceived = true; + }) + testClass.propertyWithNotifySignalChanged.connect(function() { + testClass.notifySignalReceived = true; + }) + testClass.myCustomNotifySignal.connect(function() { + testClass.customNotifySignalReceived = true; + }) + testClass.triggerDefaultSignal(); + testClass.triggerNotifySignal(); + testClass.triggerCustomNotifySignal(); + "); + + Instance.DefaultSignalReceived.Should().Be(true); + Instance.NotifySignalReceived.Should().Be(true); + Instance.CustomNotifySignalReceived.Should().Be(true); + } + } +} \ No newline at end of file diff --git a/src/net/Qml.Net.Tests/Qml/BaseQmlTests.cs b/src/net/Qml.Net.Tests/Qml/BaseQmlTests.cs index 765ab8ac..763d725d 100644 --- a/src/net/Qml.Net.Tests/Qml/BaseQmlTests.cs +++ b/src/net/Qml.Net.Tests/Qml/BaseQmlTests.cs @@ -76,8 +76,9 @@ namespace Qml.Net.Tests.Qml { protected readonly Mock Mock; - protected BaseQmlTests() + protected BaseQmlTests(bool enableAutoSignals = false) { + QmlNetConfig.AutoGenerateNotifySignals = enableAutoSignals; RegisterType(); Mock = new Mock(); TypeCreator.SetInstance(typeof(T), Mock.Object); @@ -89,8 +90,9 @@ namespace Qml.Net.Tests.Qml { protected readonly T Instance; - protected BaseQmlTestsWithInstance() + protected BaseQmlTestsWithInstance(bool enableAutoSignals = false) { + QmlNetConfig.AutoGenerateNotifySignals = enableAutoSignals; RegisterType(); Instance = new T(); TypeCreator.SetInstance(typeof(T), Instance); @@ -104,8 +106,8 @@ namespace Qml.Net.Tests.Qml protected BaseQmlMvvmTestsWithInstance(bool autogenerateSignals = false) { - QmlNetConfig.AutoGenerateNotifySignals = autogenerateSignals; InteropBehaviors.ClearQmlInteropBehaviors(); + QmlNetConfig.AutoGenerateNotifySignals = autogenerateSignals; InteropBehaviors.RegisterQmlInteropBehavior(new MvvmQmlInteropBehavior()); RegisterType(); diff --git a/src/net/Qml.Net/Internal/Behaviors/AutoGenerateNotifySignalsBehavior.cs b/src/net/Qml.Net/Internal/Behaviors/AutoGenerateNotifySignalsBehavior.cs new file mode 100644 index 00000000..63ae432d --- /dev/null +++ b/src/net/Qml.Net/Internal/Behaviors/AutoGenerateNotifySignalsBehavior.cs @@ -0,0 +1,71 @@ +using System; +using Qml.Net.Internal.Types; + +namespace Qml.Net.Internal.Behaviors +{ + // All properties have a default notify signal. + // This is so that when we read properties in QML, + // we don't get errors with "NON-NOTIFY PROPERTY BOUND + public class AutoGenerateNotifySignalsBehavior : IQmlInteropBehavior + { + public int Priority => 1000; + + public bool IsApplicableFor(Type type) + { + return true; + } + + void IQmlInteropBehavior.OnNetTypeInfoCreated(NetTypeInfo netTypeInfo, Type forType) + { + if (!IsApplicableFor(forType)) + { + return; + } + for (var i = 0; i < netTypeInfo.PropertyCount; i++) + { + int? existingSignalIndex = null; + + var property = netTypeInfo.GetProperty(i); + if (property.NotifySignal != null) + { + // In this case some other behavior or the user has already set up a notify signal for this property. + // We don't want to destroy that. + continue; + } + var signalName = $"dynamic__{property.Name}Changed"; + + // Check if this signal already has been registered. + for (var signalIndex = 0; signalIndex < netTypeInfo.SignalCount; signalIndex++) + { + var signal = netTypeInfo.GetSignal(signalIndex); + if (string.Equals(signalName, signal.Name)) + { + existingSignalIndex = signalIndex; + break; + } + } + if (existingSignalIndex.HasValue) + { + // Signal for this property is already existent but not registered (we check that above). + property.NotifySignal = netTypeInfo.GetSignal(existingSignalIndex.Value); + continue; + } + + // Create a new signal and link it to the property. + var notifySignalInfo = new NetSignalInfo(netTypeInfo, signalName); + netTypeInfo.AddSignal(notifySignalInfo); + property.NotifySignal = notifySignalInfo; + } + } + + public void OnObjectEntersNative(object instance, ulong objectId) + { + // NOOP + } + + public void OnObjectLeavesNative(object instance, ulong objectId) + { + // NOOP + } + } +} \ No newline at end of file diff --git a/src/net/Qml.Net/Internal/Behaviors/MvvmQmlInteropBehavior.cs b/src/net/Qml.Net/Internal/Behaviors/MvvmQmlInteropBehavior.cs index 02dc7e0c..ae369b44 100644 --- a/src/net/Qml.Net/Internal/Behaviors/MvvmQmlInteropBehavior.cs +++ b/src/net/Qml.Net/Internal/Behaviors/MvvmQmlInteropBehavior.cs @@ -18,7 +18,7 @@ namespace Qml.Net.Internal.Behaviors // ReSharper disable once MemberCanBePrivate.Local // ReSharper disable once UnusedAutoPropertyAccessor.Local public string Name { get; } - + public string SignalName { get; } } @@ -42,6 +42,8 @@ namespace Qml.Net.Internal.Behaviors private static readonly Dictionary TypeInfos = new Dictionary(); + public int Priority => 1; + public bool IsApplicableFor(Type type) { return typeof(INotifyPropertyChanged).IsAssignableFrom(type); @@ -82,7 +84,7 @@ namespace Qml.Net.Internal.Behaviors { return; } - + // Fire signal according to the property that got changed. var type = sender.GetType(); if (TypeInfos.TryGetValue(type, out var typeInfo)) @@ -127,7 +129,7 @@ namespace Qml.Net.Internal.Behaviors } var signalName = CalculateSignalNameFromPropertyName(property.Name); mvvmTypeInfo.AddPropertyInfo(property.Name, signalName); - + // Check if this signal already has been registered. for (var signalIndex = 0; signalIndex < netTypeInfo.SignalCount; signalIndex++) { @@ -144,7 +146,7 @@ namespace Qml.Net.Internal.Behaviors property.NotifySignal = netTypeInfo.GetSignal(existingSignalIndex.Value); continue; } - + // Create a new signal and link it to the property. var notifySignalInfo = new NetSignalInfo(netTypeInfo, signalName); netTypeInfo.AddSignal(notifySignalInfo); diff --git a/src/net/Qml.Net/Internal/DefaultCallbacks.cs b/src/net/Qml.Net/Internal/DefaultCallbacks.cs index aa7fe9a3..5db1d436 100644 --- a/src/net/Qml.Net/Internal/DefaultCallbacks.cs +++ b/src/net/Qml.Net/Internal/DefaultCallbacks.cs @@ -147,18 +147,6 @@ namespace Qml.Net.Internal NetSignalInfo notifySignal = null; var notifySignalAttribute = propertyInfo.GetCustomAttribute(); - // All properties have a default notify signal. - // This is so that when we read properties in QML, - // we don't get errors with "NON-NOTIFY PROPERTY BOUND - if (notifySignalAttribute == null && QmlNetConfig.AutoGenerateNotifySignals) - { - var dynamicName = $"dynamic__{propertyInfo.Name}Changed"; - notifySignalAttribute = new NotifySignalAttribute - { - Name = dynamicName - }; - } - if (notifySignalAttribute != null) { var name = notifySignalAttribute.Name; diff --git a/src/net/Qml.Net/Internal/IQmlInteropBehavior.cs b/src/net/Qml.Net/Internal/IQmlInteropBehavior.cs index 30fb46e2..649ac599 100644 --- a/src/net/Qml.Net/Internal/IQmlInteropBehavior.cs +++ b/src/net/Qml.Net/Internal/IQmlInteropBehavior.cs @@ -5,6 +5,11 @@ namespace Qml.Net.Internal { internal interface IQmlInteropBehavior { + /// + /// Gets the priority of this behavior (lower is more prior) + /// + int Priority { get; } + bool IsApplicableFor(Type type); void OnNetTypeInfoCreated(NetTypeInfo netTypeInfo, Type forType); diff --git a/src/net/Qml.Net/Internal/InteropBehaviors.cs b/src/net/Qml.Net/Internal/InteropBehaviors.cs index 9be16210..b8ce2dcf 100644 --- a/src/net/Qml.Net/Internal/InteropBehaviors.cs +++ b/src/net/Qml.Net/Internal/InteropBehaviors.cs @@ -8,13 +8,14 @@ namespace Qml.Net.Internal internal static class InteropBehaviors { private static List _QmlInteropBehaviors = new List(); - + public static IEnumerable QmlInteropBehaviors => _QmlInteropBehaviors; private static IEnumerable GetApplicableInteropBehaviors(Type forType) { return _QmlInteropBehaviors - .Where(b => b.IsApplicableFor(forType)); + .Where(b => b.IsApplicableFor(forType)) + .OrderBy(b => b.Priority); } /// @@ -33,6 +34,11 @@ namespace Qml.Net.Internal } } + public static void RemoveQmlInteropBehavior() + { + _QmlInteropBehaviors.RemoveAll(b => typeof(TBehavior).IsAssignableFrom(b.GetType())); + } + public static void ClearQmlInteropBehaviors() { _QmlInteropBehaviors.Clear(); diff --git a/src/net/Qml.Net/QmlNetConfig.cs b/src/net/Qml.Net/QmlNetConfig.cs index 3ae9a27e..ec425371 100644 --- a/src/net/Qml.Net/QmlNetConfig.cs +++ b/src/net/Qml.Net/QmlNetConfig.cs @@ -1,4 +1,6 @@ using System; +using Qml.Net.Internal; +using Qml.Net.Internal.Behaviors; namespace Qml.Net { @@ -18,8 +20,23 @@ namespace Qml.Net public static bool ShouldEnsureUIThread { get; set; } = true; - public static bool AutoGenerateNotifySignals { get; set; } = false; - + public static bool AutoGenerateNotifySignals + { + get => _autoGenerateNotifySignals; + set + { + _autoGenerateNotifySignals = value; + if (value) + { + InteropBehaviors.RegisterQmlInteropBehavior(new AutoGenerateNotifySignalsBehavior()); + } + else + { + InteropBehaviors.RemoveQmlInteropBehavior(); + } + } + } + public static Action EnsureUIThreadDelegate = () => { if (!QCoreApplication.IsMainThread) @@ -33,6 +50,8 @@ Stack Trace: } }; + private static bool _autoGenerateNotifySignals = false; + internal static void EnsureUIThread() { if (!ShouldEnsureUIThread)