mirror of
https://github.com/ppy/osu.git
synced 2024-09-22 06:47:24 +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;
|
||||
}
|
||||
|
||||
[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]
|
||||
public void TestSubscriptionWithContextLoss()
|
||||
{
|
||||
@ -163,5 +185,41 @@ namespace osu.Game.Tests.Database
|
||||
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.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
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>
|
||||
/// Run work on realm that will be run every time the update thread realm instance gets recycled.
|
||||
/// </summary>
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -86,26 +85,10 @@ namespace osu.Game.Screens.Play
|
||||
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
|
||||
|
||||
beatmapOffsetSubscription = realm.RegisterCustomSubscription(r =>
|
||||
{
|
||||
var userSettings = r.Find<BeatmapInfo>(beatmap.BeatmapInfo.ID)?.UserSettings;
|
||||
|
||||
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;
|
||||
});
|
||||
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
|
||||
r => r.Find<BeatmapInfo>(beatmap.BeatmapInfo.ID)?.UserSettings,
|
||||
settings => settings.Offset,
|
||||
val => userBeatmapOffsetClock.Offset = val);
|
||||
|
||||
// sane default provided by ruleset.
|
||||
startOffset = gameplayStartTime;
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -120,24 +119,10 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
ReferenceScore.BindValueChanged(scoreChanged, true);
|
||||
|
||||
beatmapOffsetSubscription = realm.RegisterCustomSubscription(r =>
|
||||
{
|
||||
var userSettings = r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
|
||||
r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
|
||||
settings => settings.Offset,
|
||||
val => Current.Value = val);
|
||||
|
||||
Current.BindValueChanged(currentChanged);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user