3

Suppose I have the following class:

public class Person : ReactiveObject, IEditableObject
{
    private string name;
    private string nameCopy;

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

    public void BeginEdit()
    {
        this.nameCopy = this.name;
    }

    public void CancelEdit()
    {
        this.name = this.nameCopy;
    }

    public void EndEdit()
    {
    }
}

Now suppose I want to create an observable sequence (of Unit) that "ticks" whenever a change is committed to Name. That is, I only care about changes to Name that occur between a call to BeginEdit and and a subsequent call to EndEdit. Any changes prior to a call to CancelEdit should be ignored and the sequence should not tick.

I'm struggling to get my head around how I would do this with Rx. It seems I would need state in the pipeline somewhere in order to know whether the change occurred during the window of BeginEdit/EndEdit calls. I suppose I could timestamp everything and compare timestamps, but that seems a nasty hack.

I came pretty close using a dedicated Subject for edit actions along with Observable.Merge:

public class Person : ReactiveObject, IEditableObject
{
    private readonly Subject<EditAction> editActions;
    private readonly IObservable<Unit> changedDuringEdit;
    private string name;
    private string nameCopy;

    public Person()
    {
        this.editActions = new Subject<EditAction>();

        var nameChanged = this.ObservableForProperty(x => x.Name).Select(x => x.Value);
        var editBeginning = this.editActions.Where(x => x == EditAction.Begin);
        var editCommitted = this.editActions.Where(x => x == EditAction.End);

        this.changedDuringEdit = nameChanged
            .Buffer(editBeginning, _ => editCommitted)
            .Where(x => x.Count > 0)
            .Select(_ => Unit.Default);
    }

    public IObservable<Unit> ChangedDuringEdit
    {
        get { return this.changedDuringEdit; }
    }

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

    public void BeginEdit()
    {
        this.editActions.OnNext(EditAction.Begin);
        this.nameCopy = this.name;
    }

    public void CancelEdit()
    {
        this.editActions.OnNext(EditAction.Cancel);
        this.Name = this.nameCopy;
    }

    public void EndEdit()
    {
        this.editActions.OnNext(EditAction.End);
    }

    private enum EditAction
    {
        Begin,
        Cancel,
        End
    }
}

However, if several changes are cancelled, and then one is committed, the observable ticks several times on commit (once for each prior cancellation, and once again for the commit). Not to mention the fact that I get a List<Unit> which I don't actually need. In a way, this would still satisfy my use case, but not my curiosity or sense of code aesthetic.

I feel like Join should solve this fairly elegantly:

var nameChanged = this.ObservableForProperty(x => x.Name).Select(_ => Unit.Default);
var editBeginning = this.editActions.Where(x => x == EditAction.Begin);
var editCommitted = this.editActions.Where(x => x == EditAction.End);
var editCancelled = this.editActions.Where(x => x == EditAction.Cancel);
var editCancelledOrCommitted = editCancelled.Merge(editCommitted);

this.changedDuringEdit = editBeginning
    .Join(nameChanged, _ => editCancelledOrCommitted, _ => editCancelledOrCommitted, (editAction, _) => editAction == EditAction.End)
    .Where(x => x)
    .Select(_ => Unit.Default);

But this doesn't work either. It seems Join is not subscribing to editCancelledOrCommitted, for reasons I don't understand.

Anyone have any ideas how to go about this cleanly?

4

2 回答 2

3

Here's how I'd do it:

IObservable<Unit> beginEditSignal = ...;
IObservable<Unit> commitSignal = ...;
IObservable<Unit> cancelEditSignal = ...;
IObservable<T> propertyChanges = ...;


// this will yield an array after each commit
// that has all of the changes for that commit.
// nothing will be yielded if the commit is canceled
// or if the changes occur before BeginEdit.
IObservable<T[]> commitedChanges = beginEditSignal
    .Take(1)
    .SelectMany(_ => propertyChanges
        .TakeUntil(commitSignal)
        .ToArray()
        .Where(changeList => changeList.Length > 0)
        .TakeUntil(cancelEditSignal))
    .Repeat();


// if you really only want a `Unit` when something happens
IObservable<Unit> changeCommittedSignal = beginEditSignal
     .Take(1)
     .SelectMany(_ => propertyChanges
         .TakeUntil(commitSignal)
         .Count()
         .Where(c => c > 0)
         .Select(c => Unit.Default)
         .TakeUntil(cancelEditSignal))
     .Repeat();
于 2013-08-28T16:35:11.990 回答
1

You have a timing problem that I don't think you have articulated yet; when are you hoping for the changes to tick?

  1. either as they occur
  2. once the commit happens

The clear and obvious problem with 1) is that you don't know if the changes will be committed, so why would you raise them. IMO, this only leaves option 2). If the change is cancelled, then no event is raised.

Next question I have is, do you want each change raised? ie. for the process

[Begin]-->[Name="fred"]-->[Name="bob"]-->[Commit]

Should this raise 1 or 2 events when the Commit is made? As you are only pushing the token type Unit, it seems redundant to push two values. This now leads me to think that you just want to push a Unit value when EndEdit() is executed and the values have changed.

This leaves us with a painfully simple implementation:

public class Person : ReactiveObject, IEditableObject
{
    private readonly ISubject<Unit> changedDuringEdit = new Subject<Unit>();
    private string name;
    private string nameCopy;


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

    public void BeginEdit()
    {
        this.nameCopy = this.name;
    }

    public void CancelEdit()
    {
        this.name = this.nameCopy;
    }

    public void EndEdit()
    {
        if(!string.Equals(this.nameCopy, this.name))
        {
            changedDuringEdit.OnNext(Unit.Default);
        }
    }

    public IObservable<Unit> ChangedDuringEdit
    {
        get { return this.changedDuringEdit.AsObservable(); }
    }
}

Is this what you are looking for? If not can you help me understand the complexities I am missing? If it is then I would be keen to flesh this out so that I wasn't recommending using Subjects :-)

于 2013-08-28T15:26:04.080 回答