开发者

Using Rx for context menu actions

开发者 https://www.devze.com 2023-03-16 19:55 出处:网络
I thought I\'d experiment with the new System.Reactive bits to see if it would simplify performing an action on response to a context menu click on an item in a ListView.

I thought I'd experiment with the new System.Reactive bits to see if it would simplify performing an action on response to a context menu click on an item in a ListView. So far all the setup bits seem a bit easier, but I'm stuggling to combine the two event streams (item selection and menu click) in the right way.

This is what I have so far (bookListView contains my books, and displays the menu bookListContextMenu)

private void frmBookList_Load(object sender, EventArgs e)
{
    //filter click events to right clicks over a ListViewItem containing a actual book
    var rightclicks = from click in Observable.FromEventPattern<MouseEventArgs>(bookListView, "MouseClick")
                      where click.EventArgs.Button == MouseButtons.Right &&
                      ClickedOnBook((ListView)click.Sender, click.EventArgs) != null
                      select click;

    //subscribe to clicks to display context menu at clicked location
    rightclicks.Subscribe(click => bookListContextMenu.Show((Control)click.Sender, click.EventArgs.Location));
    //subscribe to clicks again to convert click into clicked book
    var rightclickedbook = rightclicks.Select(click => ClickedOnBook((ListView)click.Sender, click.EventArgs));

    //capture context menu click, convert to action enum
    var clickaction = Observable.FromEventPattern<ToolStripItemClickedEventArgs>(bookListContextMenu, "ItemClicked")
                          .Select(click => GetAction(click.EventArgs.ClickedItem));

    //combine the two event streams to get a pair of Book and Action
    //can project to an anonymoue type as it will be consumed within this method
    var bookaction = clickaction.CombineLatest(rightclickedbook, (action, book) => new { Action = action, Book = book });

    //subscribe to action and branch to action specific method
    bookaction.Subscribe(doaction =>
    {
        switch (doaction.Action)
        {
            case BookAction.Delete:
                DeleteBookCommand(doaction.Book);
                break;
            case BookAction.Edit:
                EditBookCommand(doaction.Book);
                break;
        }
    });
}

public enum BookAction
{
    None = 0,
    Edit = 1,
    Delete = 2
}

private BookAction GetAction(ToolStripItem item)
{
    if (item == deleteBookToolStripMenuItem) return BookAction.Delete;
    else if (item == editBookToolStripMenuItem) return BookAction.Edit;
    else return BookAction.None;
}

private Book ClickedOnBook(ListView lview, MouseEventArgs click)
{
    ListViewItem lvitem = lview.GetItemAt(click.X, click.Y);
    return lvitem != null ? lvitem.Tag as Book : null;
}

private void DeleteBookCommand(Book selectedbook)
{
   //code to delete a book
}

private void EditBookCommand(Book selectedbook)
{
   //code to edit a book
}
开发者_Python百科

The problem is the combining function. If I use 'CombineLatest' then after the first use of the context menu each subsequent right-click invokes the previous action again on the new selection.

If I use 'Zip' and then right-click on a book but click away from the context menu rather than on it then the next time I right-click and do actually click on the menu the action is invoked on the first selection and not the second one.

I tried various forms of timebound buffers and windows and latest etc. but usually only succeeded in either blocking as soon as the menu appeared so that no selection was possible, or getting an exception about an empty sequence if the menu was shown but no item had been clicked.

I'm sure there must be an easier way to do this that I'm missing but I'm not sure what it is.

Perhaps this?

//the menu may be closed with or without clicking any item
var contextMenuClosed = Observable.FromEventPattern(bookListContextMenu, "Closed");
var contextMenuClicked = Observable.FromEventPattern<ToolStripItemClickedEventArgs>(bookListContextMenu, "ItemClicked");

//combine the two event streams to get a pair of Book and Action
//which we can project to an anonymoue type as it will be consumed within this method
var bookaction = from mouseclick in rightclicks
                 let book = ClickedOnBook((ListView)mouseclick.Sender, mouseclick.EventArgs)
                 from menuclick in contextMenuClicked.Take(1).TakeUntil(contextMenuClosed)
                 let action = GetAction(menuclick.EventArgs.ClickedItem)
                 select new { Action = action, Book = book };


The easiest way to solve those is to carry the causing element (the one you get from ClickedOnBook) through the entire observable sequence. One way to do it is to create a separate instance of the context menu each time somebody clicks on the book. Something like

IObservable<BookAction> WhenBookAction()
{
    return Observable.Defer(() => 
    {
       var menu = ContextMenuFactory.CreateMenu;
       return Observable
         .FromEventPattern<ToolStripItemClickedEventArgs>(
            menu, "ItemClicked")
         .Select(click => GetAction(click.EventArgs.ClickedItem));
    }
}

Now a simple "from/from" would do:

from book in rightclicks.Select(
  click => ClickedOnBook((ListView)click.Sender, click.EventArgs))
from action in WhenBookAction()
select book, action;

You're guaranteed that the book in the pair is exactly the one that had caused the menu to appear - the "from/from" AKA "SelectMany" AKA "Monad" takes care of that.

EDIT: Here's another nice way to do it. I was lazy to replicate the context menu, so my "test suite" is a window with three buttons named "firstBn", "secondBn" and "actionBn" - this is the same pattern as in this question. Here is what I got:

    public MainWindow()
    {
        InitializeComponent();
        var actions = Observable
            .FromEventPattern<RoutedEventArgs>(actionBn, "Click");
        var firsts = Observable
            .FromEventPattern<RoutedEventArgs>(firstBn, "Click")
            .Select(x => firstBn);
        var seconds = Observable
            .FromEventPattern<RoutedEventArgs>(secondBn, "Click")
            .Select(x => secondBn);
        var buttons = firsts.Merge(seconds);
        var buttonActions = buttons
            .Select(x => actions.Select(_ => x))
            .Switch();
        buttonActions.Subscribe(x => Console.WriteLine(x.Name));
    }
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号