Использование Rx для действий контекстного меню - PullRequest
3 голосов
/ 03 июля 2011

Я думал, что поэкспериментирую с новыми битами System.Reactive, чтобы увидеть, упростит ли это выполнение действия в ответ на щелчок контекстного меню над элементом в ListView.Пока что все биты настройки кажутся немного проще, но я изо всех сил пытаюсь правильно объединить два потока событий (выбор элемента и нажатие в меню).

Это то, что у меня пока есть (bookListView содержитмои книги и отображает меню 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
}

Проблема заключается в функции объединения.Если я использую «CombineLatest», то после первого использования контекстного меню каждый последующий щелчок правой кнопкой снова вызывает предыдущее действие для нового выделения.

Если я использую «Zip», а затем щелкните правой кнопкой мыши книгуно щелкните в контекстном меню, а не в нем, затем в следующий раз, когда я щелкну правой кнопкой мыши и действительно нажму на меню, действие вызывается для первого выбора, а не для второго.

Я пробовал различные формывременных буферов и окон, а также последних и т. д., но обычно удается либо блокировать, как только появляется меню, чтобы сделать выбор невозможным, либо получать исключение в отношении пустой последовательности, если меню отображалось, но ни один элемент не был нажат.

Я уверен, что должен быть более простой способ сделать это, чего мне не хватает, но я не уверен, что это такое.

Возможно, это?

//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 };

1 Ответ

2 голосов
/ 03 июля 2011

Самый простой способ решить эти проблемы - провести вызывающий элемент (тот, который вы получаете из ClickedOnBook) через всю наблюдаемую последовательность. Один из способов сделать это - создать отдельный экземпляр контекстного меню каждый раз, когда кто-нибудь нажимает на книгу. Что-то вроде

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

Теперь простое «из / из» будет делать:

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

Вам гарантировано, что книга в паре именно та, из-за которой появилось меню - об этом позаботится "from / from" AKA "SelectMany" AKA "Monad".

РЕДАКТИРОВАТЬ: Вот еще один хороший способ сделать это. Мне было лениво копировать контекстное меню, поэтому мой «набор тестов» - это окно с тремя кнопками с именами «firstBn», «secondBn» и «actionBn» - это тот же шаблон, что и в этом вопросе. Вот что я получил:

    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));
    }
...