Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/rxui6-master' into intellisense-…
Browse files Browse the repository at this point in the history
…docs
  • Loading branch information
anaisbetts committed Jul 3, 2014
2 parents 970b37d + b8883b6 commit 02ba113
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 10 deletions.
41 changes: 41 additions & 0 deletions ReactiveUI.Tests/ReactiveObjectTest.cs
Expand Up @@ -199,5 +199,46 @@ public void ExceptionsThrownInSubscribersShouldMarshalToThrownExceptions()
fixture.IsOnlyOneWord = "Bar";
Assert.Equal(1, exceptionList.Count);
}

[Fact]
public void DeferringNotificationsDontShowUpUntilUndeferred()
{
var fixture = new TestFixture();
var output = fixture.Changed.CreateCollection();

Assert.Equal(0, output.Count);
fixture.NullableInt = 4;
Assert.Equal(1, output.Count);

var stopDelaying = fixture.DelayChangeNotifications();

fixture.NullableInt = 5;
Assert.Equal(1, output.Count);

fixture.IsNotNullString = "Bar";
Assert.Equal(1, output.Count);

fixture.NullableInt = 6;
Assert.Equal(1, output.Count);

fixture.IsNotNullString = "Baz";
Assert.Equal(1, output.Count);

var stopDelayingMore = fixture.DelayChangeNotifications();

fixture.IsNotNullString = "Bamf";
Assert.Equal(1, output.Count);

stopDelaying.Dispose();

fixture.IsNotNullString = "Blargh";
Assert.Equal(1, output.Count);

// NB: Because we debounce queued up notifications, we should only
// see a notification from the latest NullableInt and the latest
// IsNotNullableString
stopDelayingMore.Dispose();
Assert.Equal(3, output.Count);
}
}
}
1 change: 1 addition & 0 deletions ReactiveUI.XamForms/ReactiveUI.XamForms.csproj
Expand Up @@ -78,6 +78,7 @@
<Compile Include="XamForms\ReactiveContentView.cs" />
<Compile Include="XamForms\Registrations.cs" />
<Compile Include="XamForms\ReactiveContentPage.cs" />
<Compile Include="XamForms\DeviceScheduler.cs" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Expand Down
63 changes: 63 additions & 0 deletions ReactiveUI.XamForms/XamForms/DeviceScheduler.cs
@@ -0,0 +1,63 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using ReactiveUI;
using Xamarin.Forms;
using Splat;

namespace ReactiveUI
{
/// <summary>
/// Scheduler that uses the Device static class to schedule items to the
/// UI thread
/// </summary>
public class DeviceScheduler : IScheduler, IEnableLogger
{
public DateTimeOffset Now {
get { return DateTimeOffset.Now; }
}

public IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
{
IDisposable innerDisp = Disposable.Empty;
Device.BeginInvokeOnMainThread(new Action(() => innerDisp = action(this, state)));

return innerDisp;
}

public IDisposable Schedule<TState>(TState state, DateTimeOffset dueTime, Func<IScheduler, TState, IDisposable> action)
{
if (dueTime <= Now) {
return Schedule(state, action);
}

return Schedule(state, dueTime - Now, action);
}

public IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
{
bool isCancelled = false;
IDisposable disp = Disposable.Empty;

Device.StartTimer(dueTime, () => {
if (!isCancelled) {
Device.BeginInvokeOnMainThread(() => {
if (!isCancelled) disp = action(this, state);
});
}
return false;
});

return Disposable.Create(() => {
isCancelled = true;
disp.Dispose();
});
}
}
}

// vim: tw=120 ts=4 sw=4 et :
1 change: 1 addition & 0 deletions ReactiveUI.XamForms/XamForms/Registrations.cs
Expand Up @@ -21,6 +21,7 @@ public void Register(Action<Func<object>, Type> registerFunction)
{
registerFunction(() => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher));
registerFunction(() => new XamlDefaultPropertyBinding(), typeof(IDefaultPropertyBindingProvider));
RxApp.MainThreadScheduler = new DeviceScheduler();
}
}
}
90 changes: 82 additions & 8 deletions ReactiveUI/IReactiveObject.cs
Expand Up @@ -9,6 +9,7 @@
using System.ComponentModel;
using Splat;
using System.Collections.Generic;
using System.Reactive;

namespace ReactiveUI
{
Expand Down Expand Up @@ -76,6 +77,13 @@ public static class IReactiveObjectExtensions
return s.areChangeNotificationsEnabled();
}

internal static IDisposable delayChangeNotifications<TSender>(this TSender This) where TSender : IReactiveObject
{
var s = state.GetValue(This, key => (IExtensionState<IReactiveObject>)new ExtensionState<TSender>(This));

return s.delayChangeNotifications();
}

/// <summary>
/// RaiseAndSetIfChanged fully implements a Setter for a read-write
/// property on a ReactiveObject, using CallerMemberName to raise the notification
Expand Down Expand Up @@ -136,14 +144,37 @@ public static void RaisePropertyChanging(this IReactiveObject This, [CallerMembe
This.raisePropertyChanging(propertyName);
}

// Filter a list of change notifications, returning the last change for each PropertyName in original order.
static IEnumerable<IReactivePropertyChangedEventArgs<TSender>> dedup<TSender>(IList<IReactivePropertyChangedEventArgs<TSender>> batch) {
if (batch.Count <= 1) {
return batch;
}

var seen = new HashSet<string>();
var unique = new LinkedList<IReactivePropertyChangedEventArgs<TSender>>();

for (int i = batch.Count - 1; i >= 0; i--) {
if (seen.Add(batch[i].PropertyName)) {
unique.AddFirst(batch[i]);
}
}

return unique;
}

class ExtensionState<TSender> : IExtensionState<TSender> where TSender : IReactiveObject
{
private long changeNotificationsSuppressed;
private ISubject<IReactivePropertyChangedEventArgs<TSender>> changingSubject;
private ISubject<IReactivePropertyChangedEventArgs<TSender>> changedSubject;
private ISubject<Exception> thrownExceptions;

private TSender sender;
long changeNotificationsSuppressed;
long changeNotificationsDelayed;
ISubject<IReactivePropertyChangedEventArgs<TSender>> changingSubject;
IObservable<IReactivePropertyChangedEventArgs<TSender>> changingObservable;
ISubject<IReactivePropertyChangedEventArgs<TSender>> changedSubject;
IObservable<IReactivePropertyChangedEventArgs<TSender>> changedObservable;
ISubject<IReactivePropertyChangedEventArgs<TSender>> fireChangedBatchSubject;
ISubject<Exception> thrownExceptions;
ISubject<Unit> startDelayNotifications;

TSender sender;

/// <summary>
/// Initializes a new instance of the <see cref="ExtensionState{TSender}"/> class.
Expand All @@ -153,15 +184,36 @@ public ExtensionState(TSender sender)
this.sender = sender;
this.changingSubject = new Subject<IReactivePropertyChangedEventArgs<TSender>>();
this.changedSubject = new Subject<IReactivePropertyChangedEventArgs<TSender>>();
this.startDelayNotifications = new Subject<Unit>();
this.thrownExceptions = new ScheduledSubject<Exception>(Scheduler.Immediate, RxApp.DefaultExceptionHandler);

this.changedObservable = changedSubject
.Buffer(
Observable.Merge(
changedSubject.Where(_ => !areChangeNotificationsDelayed()).Select(_ => Unit.Default),
startDelayNotifications)
)
.SelectMany(batch => dedup(batch))
.Publish()
.RefCount();

this.changingObservable = changingSubject
.Buffer(
Observable.Merge(
changingSubject.Where(_ => !areChangeNotificationsDelayed()).Select(_ => Unit.Default),
startDelayNotifications)
)
.SelectMany(batch => dedup(batch))
.Publish()
.RefCount();
}

public IObservable<IReactivePropertyChangedEventArgs<TSender>> Changing {
get { return this.changingSubject; }
get { return this.changingObservable; }
}

public IObservable<IReactivePropertyChangedEventArgs<TSender>> Changed {
get { return this.changedSubject; }
get { return this.changedObservable; }
}

public IObservable<Exception> ThrownExceptions {
Expand All @@ -173,6 +225,11 @@ public bool areChangeNotificationsEnabled()
return (Interlocked.Read(ref changeNotificationsSuppressed) == 0);
}

public bool areChangeNotificationsDelayed()
{
return (Interlocked.Read(ref changeNotificationsDelayed) > 0);
}

/// <summary>
/// When this method is called, an object will not fire change
/// notifications (neither traditional nor Observable notifications)
Expand All @@ -186,6 +243,19 @@ public IDisposable suppressChangeNotifications()
return Disposable.Create(() => Interlocked.Decrement(ref changeNotificationsSuppressed));
}

public IDisposable delayChangeNotifications()
{
if (Interlocked.Increment(ref changeNotificationsDelayed) == 1) {
startDelayNotifications.OnNext(Unit.Default);
}

return Disposable.Create(() => {
if (Interlocked.Decrement(ref changeNotificationsDelayed) == 0) {
startDelayNotifications.OnNext(Unit.Default);
};
});
}

public void raisePropertyChanging(string propertyName)
{
if (!this.areChangeNotificationsEnabled())
Expand Down Expand Up @@ -234,6 +304,10 @@ interface IExtensionState<out TSender> where TSender: IReactiveObject
bool areChangeNotificationsEnabled();

IDisposable suppressChangeNotifications();

bool areChangeNotificationsDelayed();

IDisposable delayChangeNotifications();
}
}
}
2 changes: 1 addition & 1 deletion ReactiveUI/PropertyBinding.cs
Expand Up @@ -876,7 +876,7 @@ public class PropertyBinderImplementation : IPropertyBinderImplementation

var source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Select(x => (TProp)x).Select(selector);

IDisposable disp = bindToDirect(source, view, viewProperty, fallbackValue);
IDisposable disp = bindToDirect(source, view, viewExpression, fallbackValue);

return new ReactiveBinding<TView, TViewModel, TOut>(view, viewModel, viewExpression, vmExpression, source, BindingDirection.OneWay, disp);
}
Expand Down
19 changes: 19 additions & 0 deletions ReactiveUI/ReactiveList.cs
Expand Up @@ -645,6 +645,25 @@ protected virtual void raiseCollectionChanged(NotifyCollectionChangedEventArgs e
}

#region Super Boring IList crap

// The BinarySearch methods aren't technically on IList<T>, they're implemented straight on List<T>
// but by proxying this call we can make use of the nice built in methods that operate on the internal
// array of the inner list instead of jumping around proxying through the index.
public int BinarySearch(T item)
{
return _inner.BinarySearch(item);
}

public int BinarySearch(T item, IComparer<T> comparer)
{
return _inner.BinarySearch(item, comparer);
}

public int BinarySearch(int index, int count, T item, IComparer<T> comparer)
{
return _inner.BinarySearch(index, count, item, comparer);
}

public IEnumerator<T> GetEnumerator()
{
return _inner.GetEnumerator();
Expand Down
4 changes: 4 additions & 0 deletions ReactiveUI/ReactiveObject.cs
Expand Up @@ -86,6 +86,10 @@ protected ReactiveObject()
public bool AreChangeNotificationsEnabled() {
return this.areChangeNotificationsEnabled();
}

public IDisposable DelayChangeNotifications() {
return this.delayChangeNotifications();
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion ReactiveUI/Xaml/ActivationForViewFetcher.cs
Expand Up @@ -32,7 +32,7 @@ public int GetAffinityForView(Type view)
x => fe.Loaded -= x).Select(_ => Unit.Default);
var viewHitTestVisible = fe.WhenAnyValue(v => v.IsHitTestVisible);

var viewActivated = viewLoaded.Zip(viewHitTestVisible, (l, h) => h)
var viewActivated = viewLoaded.CombineLatest(viewHitTestVisible, (l, h) => h)
.Where(v => v)
.Select(_ => Unit.Default);

Expand Down

0 comments on commit 02ba113

Please sign in to comment.