mirror of
https://github.com/ppy/osu.git
synced 2024-09-22 09:27:34 +08:00
Merge pull request #17075 from peppy/realm-property-watching
Add ability to watch properties via a `RealmAccess` helper method
This commit is contained in:
commit
51f6eb4028
@ -47,6 +47,28 @@ namespace osu.Game.Tests.Database
|
|||||||
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => lastChanges = changes;
|
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => lastChanges = changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPropertyChangedSubscription()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realm, _) =>
|
||||||
|
{
|
||||||
|
bool? receivedValue = null;
|
||||||
|
|
||||||
|
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||||
|
|
||||||
|
using (realm.SubscribeToPropertyChanged(r => r.All<BeatmapSetInfo>().First(), setInfo => setInfo.Protected, val => receivedValue = val))
|
||||||
|
{
|
||||||
|
Assert.That(receivedValue, Is.False);
|
||||||
|
|
||||||
|
realm.Write(r => r.All<BeatmapSetInfo>().First().Protected = true);
|
||||||
|
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
|
||||||
|
Assert.That(receivedValue, Is.True);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSubscriptionWithContextLoss()
|
public void TestSubscriptionWithContextLoss()
|
||||||
{
|
{
|
||||||
@ -163,5 +185,41 @@ namespace osu.Game.Tests.Database
|
|||||||
Assert.That(beatmapSetInfo, Is.Null);
|
Assert.That(beatmapSetInfo, Is.Null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPropertyChangedSubscriptionWithContextLoss()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realm, _) =>
|
||||||
|
{
|
||||||
|
bool? receivedValue = null;
|
||||||
|
|
||||||
|
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||||
|
|
||||||
|
var subscription = realm.SubscribeToPropertyChanged(
|
||||||
|
r => r.All<BeatmapSetInfo>().First(),
|
||||||
|
setInfo => setInfo.Protected,
|
||||||
|
val => receivedValue = val);
|
||||||
|
|
||||||
|
Assert.That(receivedValue, Is.Not.Null);
|
||||||
|
receivedValue = null;
|
||||||
|
|
||||||
|
using (realm.BlockAllOperations())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-registration after context restore.
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
Assert.That(receivedValue, Is.Not.Null);
|
||||||
|
|
||||||
|
subscription.Dispose();
|
||||||
|
receivedValue = null;
|
||||||
|
|
||||||
|
using (realm.BlockAllOperations())
|
||||||
|
Assert.That(receivedValue, Is.Null);
|
||||||
|
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
Assert.That(receivedValue, Is.Null);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -318,6 +320,66 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe to the property of a realm object to watch for changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// On subscribing, unless the <paramref name="modelAccessor"/> does not match an object, an initial invocation of <paramref name="onChanged"/> will occur immediately.
|
||||||
|
/// Further invocations will occur when the value changes, but may also fire on a realm recycle with no actual value change.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="modelAccessor">A function to retrieve the relevant model from realm.</param>
|
||||||
|
/// <param name="propertyLookup">A function to traverse to the relevant property from the model.</param>
|
||||||
|
/// <param name="onChanged">A function to be invoked when a change of value occurs.</param>
|
||||||
|
/// <typeparam name="TModel">The type of the model.</typeparam>
|
||||||
|
/// <typeparam name="TProperty">The type of the property to be watched.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
|
||||||
|
/// To stop receiving notifications, call <see cref="IDisposable.Dispose"/>.
|
||||||
|
/// </returns>
|
||||||
|
public IDisposable SubscribeToPropertyChanged<TModel, TProperty>(Func<Realm, TModel?> modelAccessor, Expression<Func<TModel, TProperty>> propertyLookup, Action<TProperty> onChanged)
|
||||||
|
where TModel : RealmObjectBase
|
||||||
|
{
|
||||||
|
return RegisterCustomSubscription(r =>
|
||||||
|
{
|
||||||
|
string propertyName = getMemberName(propertyLookup);
|
||||||
|
|
||||||
|
var model = Run(modelAccessor);
|
||||||
|
var propLookupCompiled = propertyLookup.Compile();
|
||||||
|
|
||||||
|
if (model == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
model.PropertyChanged += onPropertyChanged;
|
||||||
|
|
||||||
|
// Update initial value immediately.
|
||||||
|
onChanged(propLookupCompiled(model));
|
||||||
|
|
||||||
|
return new InvokeOnDisposal(() => model.PropertyChanged -= onPropertyChanged);
|
||||||
|
|
||||||
|
void onPropertyChanged(object sender, PropertyChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.PropertyName == propertyName)
|
||||||
|
onChanged(propLookupCompiled(model));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static string getMemberName(Expression<Func<TModel, TProperty>> expression)
|
||||||
|
{
|
||||||
|
if (!(expression is LambdaExpression lambda))
|
||||||
|
throw new ArgumentException("Outermost expression must be a lambda expression", nameof(expression));
|
||||||
|
|
||||||
|
if (!(lambda.Body is MemberExpression memberExpression))
|
||||||
|
throw new ArgumentException("Lambda body must be a member access expression", nameof(expression));
|
||||||
|
|
||||||
|
// TODO: nested access can be supported, with more iteration here
|
||||||
|
// (need to iteratively soft-cast `memberExpression.Expression` into `MemberExpression`s until `lambda.Parameters[0]` is hit)
|
||||||
|
if (memberExpression.Expression != lambda.Parameters[0])
|
||||||
|
throw new ArgumentException("Nested access expressions are not supported", nameof(expression));
|
||||||
|
|
||||||
|
return memberExpression.Member.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Run work on realm that will be run every time the update thread realm instance gets recycled.
|
/// Run work on realm that will be run every time the update thread realm instance gets recycled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -86,26 +85,10 @@ namespace osu.Game.Screens.Play
|
|||||||
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
||||||
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
|
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
|
||||||
|
|
||||||
beatmapOffsetSubscription = realm.RegisterCustomSubscription(r =>
|
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
|
||||||
{
|
r => r.Find<BeatmapInfo>(beatmap.BeatmapInfo.ID)?.UserSettings,
|
||||||
var userSettings = r.Find<BeatmapInfo>(beatmap.BeatmapInfo.ID)?.UserSettings;
|
settings => settings.Offset,
|
||||||
|
val => userBeatmapOffsetClock.Offset = val);
|
||||||
if (userSettings == null) // only the case for tests.
|
|
||||||
return null;
|
|
||||||
|
|
||||||
void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
|
|
||||||
{
|
|
||||||
if (args.PropertyName == nameof(BeatmapUserSettings.Offset))
|
|
||||||
updateOffset();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOffset();
|
|
||||||
userSettings.PropertyChanged += onUserSettingsOnPropertyChanged;
|
|
||||||
|
|
||||||
return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged);
|
|
||||||
|
|
||||||
void updateOffset() => userBeatmapOffsetClock.Offset = userSettings.Offset;
|
|
||||||
});
|
|
||||||
|
|
||||||
// sane default provided by ruleset.
|
// sane default provided by ruleset.
|
||||||
startOffset = gameplayStartTime;
|
startOffset = gameplayStartTime;
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -120,24 +119,10 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
|||||||
|
|
||||||
ReferenceScore.BindValueChanged(scoreChanged, true);
|
ReferenceScore.BindValueChanged(scoreChanged, true);
|
||||||
|
|
||||||
beatmapOffsetSubscription = realm.RegisterCustomSubscription(r =>
|
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
|
||||||
{
|
r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
|
||||||
var userSettings = r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings;
|
settings => settings.Offset,
|
||||||
|
val => Current.Value = val);
|
||||||
if (userSettings == null) // only the case for tests.
|
|
||||||
return null;
|
|
||||||
|
|
||||||
Current.Value = userSettings.Offset;
|
|
||||||
userSettings.PropertyChanged += onUserSettingsOnPropertyChanged;
|
|
||||||
|
|
||||||
return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged);
|
|
||||||
|
|
||||||
void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
|
|
||||||
{
|
|
||||||
if (args.PropertyName == nameof(BeatmapUserSettings.Offset))
|
|
||||||
Current.Value = userSettings.Offset;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Current.BindValueChanged(currentChanged);
|
Current.BindValueChanged(currentChanged);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user