0

I have a few questions about a use of SelectMany I have encountered in one of the projects I am working on. Below is a small sample that reproduces its use (with a few Console.WriteLines I was using to help see the states at various points):

public partial class MainWindow : INotifyPropertyChanged
{
    private bool _cb1, _cb2, _cb3, _isDirty;
    private readonly ISubject<Unit> _cb1HasChanged = new Subject<Unit>();
    private readonly ISubject<Unit> _cb2HasChanged = new Subject<Unit>();
    private readonly ISubject<Unit> _cb3HasChanged = new Subject<Unit>();
    private readonly ISubject<string> _initialState = new ReplaySubject<string>(1);

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        ObserveCheckBoxes();

        var initialState = string.Format("{0}{1}{2}", CB1, CB2, CB3);
        _initialState.OnNext(initialState);
        Console.WriteLine("INITIAL STATE: " + initialState);
    }

    public bool CB1
    {
        get
        {
            return _cb1;
        }
        set
        {
            _cb1 = value;
            _cb1HasChanged.OnNext(Unit.Default);
        }
    }

    public bool CB2
    {
        get
        {
            return _cb2;
        }
        set
        {
            _cb2 = value;
            _cb2HasChanged.OnNext(Unit.Default);
        }
    }

    public bool CB3
    {
        get
        {
            return _cb3;
        }
        set
        {
            _cb3 = value;
            _cb3HasChanged.OnNext(Unit.Default);
        }
    }

    public bool IsDirty
    {
        get
        {
            return _isDirty;
        }
        set
        {
            _isDirty = value;
            OnPropertyChanged("IsDirty");
        }
    }

    private void ObserveCheckBoxes()
    {
        var checkBoxChanges = new[]
            {
                _cb1HasChanged,
                _cb2HasChanged,
                _cb3HasChanged
            }
            .Merge();

        var isDirty = _initialState.SelectMany(initialState => checkBoxChanges
                                                                   .Select(_ => GetNewState(initialState))
                                                                   .Select(updatedState => initialState != updatedState)
                                                                   .StartWith(false)
                                                                   .TakeUntil(_initialState.Skip(1)));
        isDirty.Subscribe(d => IsDirty = d);
    }

    private string GetNewState(string initialState = null)
    {
        string update = string.Format("{0}{1}{2}", CB1, CB2, CB3);
        if (initialState != null)
        {
            Console.WriteLine("CREATING UPDATE: " + update + " INITIAL STATE: " + initialState);
        }
        else
        {
            Console.WriteLine("CREATING UPDATE: " + update);
        }
        return update;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged(string prop)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
    }

    private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
    {
        var newState = GetNewState();
        _initialState.OnNext(newState);
        Console.WriteLine("SAVED AS: " + newState);
    }
}

and the xaml:

<Window x:Class="WpfSB2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel>
            <CheckBox IsChecked="{Binding CB1}"></CheckBox>
            <CheckBox IsChecked="{Binding CB2}"></CheckBox>
            <CheckBox IsChecked="{Binding CB3}"></CheckBox>
            <Button IsEnabled="{Binding IsDirty}" Click="Button_Click">APPLY</Button>
        </StackPanel>
    </Grid>
</Window>

So what this little app does is show three checkboxes (all initially unchecked) and an "Apply" button. When the state of the checkboxes changes, the button should become enabled and when clicked become disabled until the state of the checkboxes changes again. If you change the state of the checkboxes but then change it back to its inital state, the button will enable/disable appropriately. The app works as expected, I am just trying to figure out why/how.

Now the questions:

  • Will the SelectMany call be triggered whenever the _initialState or a check box change occurs?

  • The first call of _initialState.OnNext(initialState); (in the constructor) doesn't really do anything when it comes to the SelectMany code. I see it makes its way to the SelectMany code but nothing is actually done (I mean, if I put a breakpoint on the checkBoxChanges.Select section it breaks but nothing is actually selected). Is this because no changes have occured to any of the checkboxes yet?

  • As expected, checking any checkBox triggers the isDirty check. What exactly is happening in this SelectMany statement the first time I change a single checkbox?

  • After checking a box, the Apply button becomse enabled and I hit Apply. This causes _initialState.OnNext(newState); to be called. Similar to my first question, nothing seems to happen in the SelectMany statement. I thought with the initial state getting a new value something would get recalculated but it seems to go straight to the OnNext handler of isDirty.Subscribe(d => IsDirty = d);

  • Now that I hit Apply, _initialState.OnNext has been called twice in total. If I check a new checkbox, how does the SelectMany handle that? Does it go through all of the past states of _initialState? Are those values stored until the observable is disposed?

  • What are the StartsWith/TakeUntil/Skip lines doing? I noticed that if I remove the TakeUntil line, the app stops working correctly as the SelectMany clause starts going through all the past values of _initialState and gets confused as to which is the actual current state to compare to.

Please let me know if you need additional info.

4

1 回答 1

2

I think the key part of your problem is your understanding of SelectMany. I think it is easier to understand if you refer to SelectMany as "From one, select many".

For each value from a source sequence, SelectMany will provide zero, one, or many values from another sequence.

In your case you have the source sequence that is _initialState. Each time a value is produced from that sequence it will subscribe to the "inner sequence" provided.

To directly answer your questions:
1) When _initialState pushes a value, then the value will be passed to the SelectMany operator and will subscribe to the provided "inner sequence".

2) The fist call is putting the InitialState in the ReplaySubject's buffer. This means when you first subscribe to the _initialState sequence it will push a value immediately. Putting your break point in the GetNewState will show you this working.

3) When you check a check box, it will call the setter, which will OnNext the _cbXHasChanged subject (yuck), which will flow into the Merged sequence (checkBoxChanges) and then flow into the SelectMany delegate query.

4) Nothing will happen until the check boxes push new values (they are not replaysubjects)

5-6) Yes you have called it twice so it will run the selectMany delegate twice, but the TakeUntil will terminate the first "inner sequence" when the second "inner sequence" is kicked off.

This is all covered in detail on (my site) IntroToRx.com in the SelectMany chapter.

于 2013-05-28T11:17:24.610 回答