c# - Using Rx for context menu actions -
i thought i'd experiment new system.reactive bits see if simplify performing action on response context menu click on item in listview. far setup bits seem bit easier, i'm stuggling combine 2 event streams (item selection , menu click) in right way.
this have far (booklistview contains books, , displays menu booklistcontextmenu)
private void frmbooklist_load(object sender, eventargs e) { //filter click events right clicks on listviewitem containing actual book var rightclicks = click in observable.fromeventpattern<mouseeventargs>(booklistview, "mouseclick") click.eventargs.button == mousebuttons.right && clickedonbook((listview)click.sender, click.eventargs) != null select click; //subscribe clicks display context menu @ clicked location rightclicks.subscribe(click => booklistcontextmenu.show((control)click.sender, click.eventargs.location)); //subscribe clicks again convert click clicked book var rightclickedbook = rightclicks.select(click => clickedonbook((listview)click.sender, click.eventargs)); //capture context menu click, convert action enum var clickaction = observable.fromeventpattern<toolstripitemclickedeventargs>(booklistcontextmenu, "itemclicked") .select(click => getaction(click.eventargs.clickeditem)); //combine 2 event streams pair of book , action //can project anonymoue type consumed within method var bookaction = clickaction.combinelatest(rightclickedbook, (action, book) => new { action = action, book = book }); //subscribe action , branch 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 book : null; } private void deletebookcommand(book selectedbook) { //code delete book } private void editbookcommand(book selectedbook) { //code edit book } the problem combining function. if use 'combinelatest' after first use of context menu each subsequent right-click invokes previous action again on new selection.
if use 'zip' , right-click on book click away context menu rather on next time right-click , click on menu action invoked on first selection , not second one.
i tried various forms of timebound buffers , windows , latest etc. succeeded in either blocking menu appeared no selection possible, or getting exception empty sequence if menu shown no item had been clicked.
i'm sure there must easier way i'm missing i'm not sure is.
perhaps this?
//the menu may closed or without clicking item var contextmenuclosed = observable.fromeventpattern(booklistcontextmenu, "closed"); var contextmenuclicked = observable.fromeventpattern<toolstripitemclickedeventargs>(booklistcontextmenu, "itemclicked"); //combine 2 event streams pair of book , action //which can project anonymoue type consumed within method var bookaction = mouseclick in rightclicks let book = clickedonbook((listview)mouseclick.sender, mouseclick.eventargs) menuclick in contextmenuclicked.take(1).takeuntil(contextmenuclosed) let action = getaction(menuclick.eventargs.clickeditem) select new { action = action, book = book };
the easiest way solve carry causing element (the 1 clickedonbook) through entire observable sequence. 1 way create separate instance of context menu each time clicks on book. like
iobservable<bookaction> whenbookaction() { return observable.defer(() => { var menu = contextmenufactory.createmenu; return observable .fromeventpattern<toolstripitemclickedeventargs>( menu, "itemclicked") .select(click => getaction(click.eventargs.clickeditem)); } } now simple "from/from" do:
from book in rightclicks.select( click => clickedonbook((listview)click.sender, click.eventargs)) action in whenbookaction() select book, action; you're guaranteed book in pair 1 had caused menu appear - "from/from" aka "selectmany" aka "monad" takes care of that.
edit: here's nice way it. lazy replicate context menu, "test suite" window 3 buttons named "firstbn", "secondbn" , "actionbn" - same pattern in question. here 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)); }
Comments
Post a Comment