Skip to content

Commit

Permalink
Add methods to automatically save objects on a timer
Browse files Browse the repository at this point in the history
  • Loading branch information
anaisbetts committed Apr 29, 2013
1 parent c5bd1f6 commit a90c770
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 10 deletions.
86 changes: 86 additions & 0 deletions ReactiveUI.Tests/AutoPersistHelperTest.cs
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Reactive.Testing;
using ReactiveUI.Testing;
using Xunit;
using System.Reactive.Subjects;

namespace ReactiveUI.Tests
{
public class AutoPersistHelperTest
{
[Fact]
public void AutoPersistDoesntWorkOnNonDataContractClasses()
{
var fixture = new FakeViewModel();

bool shouldDie = true;
try {
fixture.AutoPersist(x => Observable.Return(Unit.Default));
} catch (Exception ex) {
shouldDie = false;
}

Assert.False(shouldDie);
}

[Fact]
public void AutoPersistHelperShouldntTriggerOnNonPersistableProperties()
{
(new TestScheduler()).With(sched => {
var fixture = new TestFixture();
var manualSave = new Subject<Unit>();
int timesSaved = 0;
fixture.AutoPersist(x => { timesSaved++; return Observable.Return(Unit.Default); }, manualSave, TimeSpan.FromMilliseconds(100));
// No changes = no saving
sched.AdvanceByMs(2 * 100);
Assert.Equal(0, timesSaved);
// Change to not serialized = no saving
fixture.NotSerialized = "Foo";
sched.AdvanceByMs(2 * 100);
Assert.Equal(0, timesSaved);
});
}

[Fact]
public void AutoPersistHelperSavesOnInterval()
{
(new TestScheduler()).With(sched => {
var fixture = new TestFixture();
var manualSave = new Subject<Unit>();
int timesSaved = 0;
fixture.AutoPersist(x => { timesSaved++; return Observable.Return(Unit.Default); }, manualSave, TimeSpan.FromMilliseconds(100));
// No changes = no saving
sched.AdvanceByMs(2 * 100);
Assert.Equal(0, timesSaved);
// Change = one save
fixture.IsNotNullString = "Foo";
sched.AdvanceByMs(2 * 100);
Assert.Equal(1, timesSaved);
// Two fast changes = one save
fixture.IsNotNullString = "Foo";
fixture.IsNotNullString = "Bar";
sched.AdvanceByMs(2 * 100);
Assert.Equal(2, timesSaved);
// Trigger save twice = one save
manualSave.OnNext(Unit.Default);
manualSave.OnNext(Unit.Default);
sched.AdvanceByMs(2 * 100);
Assert.Equal(3, timesSaved);
});
}
}
}
26 changes: 16 additions & 10 deletions ReactiveUI.Tests/ReactiveObjectTest.cs
Expand Up @@ -10,41 +10,41 @@ namespace ReactiveUI.Tests
[DataContract]
public class TestFixture : ReactiveObject
{
[DataMember]
public string _IsNotNullString;
[IgnoreDataMember]
string _IsNotNullString;
[DataMember]
public string IsNotNullString {
get { return _IsNotNullString; }
set { this.RaiseAndSetIfChanged(ref _IsNotNullString, value); }
}

[DataMember]
public string _IsOnlyOneWord;
[IgnoreDataMember]
string _IsOnlyOneWord;
[DataMember]
public string IsOnlyOneWord {
get { return _IsOnlyOneWord; }
set { this.RaiseAndSetIfChanged(ref _IsOnlyOneWord, value); }
}

[DataMember]
public List<string> _StackOverflowTrigger;
[IgnoreDataMember]
List<string> _StackOverflowTrigger;
[DataMember]
public List<string> StackOverflowTrigger {
get { return _StackOverflowTrigger; }
set { this.RaiseAndSetIfChanged(ref _StackOverflowTrigger, value.ToList()); }
}

[DataMember]
public string _UsesExprRaiseSet;
[IgnoreDataMember]
string _UsesExprRaiseSet;
[DataMember]
public string UsesExprRaiseSet {
get { return _UsesExprRaiseSet; }
set { this.RaiseAndSetIfChanged(ref _UsesExprRaiseSet, value); }
}

[DataMember]
public string _PocoProperty;
[IgnoreDataMember]
string _PocoProperty;
[DataMember]
public string PocoProperty {
get { return _PocoProperty; }
set { _PocoProperty = value; }
Expand All @@ -53,6 +53,12 @@ public class TestFixture : ReactiveObject
[DataMember]
public ReactiveCollection<int> TestCollection { get; protected set; }

string _NotSerialized;
public string NotSerialized {
get { return _NotSerialized; }
set { this.RaiseAndSetIfChanged(ref _NotSerialized, value); }
}

public TestFixture()
{
TestCollection = new ReactiveCollection<int>() {ChangeTrackingEnabled = true};
Expand Down
1 change: 1 addition & 0 deletions ReactiveUI.Tests/ReactiveUI.Tests_Net45.csproj
Expand Up @@ -114,6 +114,7 @@
</CodeAnalysisDependentAssemblyPaths>
</ItemGroup>
<ItemGroup>
<Compile Include="AutoPersistHelperTest.cs" />
<Compile Include="AwaiterTest.cs" />
<Compile Include="BindingTypeConvertersTest.cs" />
<Compile Include="CommandBindingTests.cs" />
Expand Down
128 changes: 128 additions & 0 deletions ReactiveUI/AutoPersistHelper.cs
@@ -0,0 +1,128 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Runtime.Serialization;
using System.Text;

namespace ReactiveUI
{
public static class AutoPersistHelper
{
static MemoizingMRUCache<Type, Dictionary<string, bool>> persistablePropertiesCache = new MemoizingMRUCache<Type, Dictionary<string, bool>>((type, _) => {
return type.GetProperties()
.Where(x => x.CustomAttributes.Any(y => typeof(DataMemberAttribute).IsAssignableFrom(y.AttributeType)))
.ToDictionary(k => k.Name, v => true);
}, 32);

static MemoizingMRUCache<Type, bool> dataContractCheckCache = new MemoizingMRUCache<Type, bool>((t, _) => {
return t.GetCustomAttributes(typeof(DataContractAttribute), true).Any();
}, 64);

public static IDisposable AutoPersist<T>(this T This, Func<T, IObservable<Unit>> doPersist, TimeSpan? interval = null)
where T : IReactiveNotifyPropertyChanged
{
return This.AutoPersist(doPersist, Observable.Never<Unit>());
}

public static IDisposable AutoPersist<T, TDontCare>(this T This, Func<T, IObservable<Unit>> doPersist, IObservable<TDontCare> manualSaveSignal, TimeSpan? interval = null)
where T : IReactiveNotifyPropertyChanged
{
interval = interval ?? TimeSpan.FromSeconds(3.0);

lock (dataContractCheckCache) {
if (!dataContractCheckCache.Get(This.GetType())) {
throw new ArgumentException("AutoPersist can only be applied to objects with [DataContract]");
}
}

var persistableProperties = default(Dictionary<string, bool>);
lock (persistablePropertiesCache) {
persistableProperties = persistablePropertiesCache.Get(This.GetType());
}

var saveHint = Observable.Merge(
This.Changed.Where(x => persistableProperties.ContainsKey(x.PropertyName)).Select(_ => Unit.Default),
manualSaveSignal.Select(_ => Unit.Default));

var autoSaver = saveHint
.Throttle(interval.Value, RxApp.TaskpoolScheduler)
.SelectMany(_ => doPersist(This))
.Publish();

// NB: This rigamarole is to prevent the initialization of a class
// from triggering a save
var ret = new SingleAssignmentDisposable();
RxApp.DeferredScheduler.Schedule(() => {
if (ret.IsDisposed) return;
ret.Disposable = autoSaver.Connect();
});

return ret;
}

public static IDisposable AutoPersistCollection<T, TDontCare>(this ReactiveCollection<T> This, Func<T, IObservable<Unit>> doPersist, IObservable<TDontCare> manualSaveSignal)
where T : IReactiveNotifyPropertyChanged
{
var disposerList = new Dictionary<T, IDisposable>();

var disp = This.ActOnEveryObject(
x => {
if (disposerList.ContainsKey(x)) return;
disposerList[x] = x.AutoPersist(doPersist, manualSaveSignal);
},
x => {
disposerList[x].Dispose();
disposerList.Remove(x);
});

return Disposable.Create(() => {
disp.Dispose();
disposerList.Values.ForEach(x => x.Dispose());
});
}

public static IDisposable ActOnEveryObject<T>(this ReactiveCollection<T> This, Action<T> onAdd, Action<T> onRemove)
where T : IReactiveNotifyPropertyChanged
{
foreach (var v in This) { onAdd(v); }

var changingDisp = This.Changing
.Where(x => x.Action == NotifyCollectionChangedAction.Reset)
.Subscribe(
_ => This.ForEach(x => onRemove(x)));

var changedDisp = This.Changed.Subscribe(x => {
switch (x.Action) {
case NotifyCollectionChangedAction.Add:
foreach (T v in x.NewItems) { onAdd(v); }
break;
case NotifyCollectionChangedAction.Replace:
foreach (T v in x.OldItems) { onRemove(v); }
foreach (T v in x.NewItems) { onAdd(v); }
break;
case NotifyCollectionChangedAction.Remove:
foreach (T v in x.OldItems) { onRemove(v); }
break;
case NotifyCollectionChangedAction.Reset:
foreach (T v in This) { onAdd(v); }
break;
default:
break;
}
});

return Disposable.Create(() => {
changingDisp.Dispose();
changedDisp.Dispose();
This.ForEach(x => onRemove(x));
});
}
}
}
1 change: 1 addition & 0 deletions ReactiveUI/ReactiveUI.csproj
Expand Up @@ -62,6 +62,7 @@
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
<Compile Include="AutoPersistHelper.cs" />
<Compile Include="BindingTypeConverters.cs" />
<Compile Include="CollectionDebugView.cs" />
<Compile Include="CommandBinding.cs" />
Expand Down

0 comments on commit a90c770

Please sign in to comment.