{"id":37633,"date":"2004-10-07T06:57:00","date_gmt":"2004-10-07T06:57:00","guid":{"rendered":"https:\/\/blogs.msdn.microsoft.com\/oldnewthing\/2004\/10\/07\/how-to-host-an-icontextmenu-part-11-composite-extensions-composition\/"},"modified":"2004-10-07T06:57:00","modified_gmt":"2004-10-07T06:57:00","slug":"how-to-host-an-icontextmenu-part-11-composite-extensions-composition","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20041007-00\/?p=37633","title":{"rendered":"How to host an IContextMenu, part 11 &#8211; Composite extensions &#8211; composition"},"content":{"rendered":"<p><P>\nOkay, now that we have two context menu handlers we want to compose\n(namely, the &#8220;real&#8221; one from the shell namespace and a &#8220;fake&#8221; one\nthat contains bonus commands we want to add), we can use merge them\ntogether by means of a composite context menu handler.\n<\/P>\n<P>\nThe kernel of the composite context menu is to multiplex multiple\ncontext menus onto a single context menu handler, using the menu identifer\noffsets to route the commands.\n<\/P>\n<P>\nEverything else is just typing.\n<\/P>\n<PRE>\nclass CCompositeContextMenu : public IContextMenu3\n{\npublic:\n  \/\/ *** IUnknown ***\n  STDMETHODIMP QueryInterface(REFIID riid, void **ppv);\n  STDMETHODIMP_(ULONG) AddRef();\n  STDMETHODIMP_(ULONG) Release();<\/p>\n<p>  \/\/ *** IContextMenu ***\n  STDMETHODIMP QueryContextMenu(HMENU hmenu,\n                          UINT indexMenu, UINT idCmdFirst,\n                          UINT idCmdLast, UINT uFlags);\n  STDMETHODIMP InvokeCommand(\n                          LPCMINVOKECOMMANDINFO lpici);\n  STDMETHODIMP GetCommandString(\n                          UINT_PTR    idCmd,\n                          UINT        uType,\n                          UINT      * pwReserved,\n                          LPSTR       pszName,\n                          UINT        cchMax);<\/p>\n<p>  \/\/ *** IContextMenu2 ***\n  STDMETHODIMP HandleMenuMsg(\n                          UINT uMsg,\n                          WPARAM wParam,\n                          LPARAM lParam);<\/p>\n<p>  \/\/ *** IContextMenu3 ***\n  STDMETHODIMP HandleMenuMsg2(\n                          UINT uMsg,\n                          WPARAM wParam,\n                          LPARAM lParam,\n                          LRESULT* plResult);<\/p>\n<p>  \/\/ Constructor\n  static HRESULT Create(IContextMenu **rgpcm, UINT cpcm,\n                        REFIID riid, void **ppv);<\/p>\n<p>private:<\/p>\n<p>  HRESULT Initialize(IContextMenu **rgpcm, UINT cpcm);\n  CCompositeContextMenu() : m_cRef(1), m_rgcmi(NULL), m_ccmi(0) { }\n  ~CCompositeContextMenu();<\/p>\n<p>  struct CONTEXTMENUINFO {\n    IContextMenu *pcm;\n    UINT cids;\n  };<\/p>\n<p>  HRESULT ReduceOrdinal(UINT_PTR *pidCmd, CONTEXTMENUINFO **ppcmi);<\/p>\n<p>private:\n  ULONG m_cRef;\n  CONTEXTMENUINFO *m_rgcmi;\n  UINT m_ccmi;\n};\n<\/PRE>\n<P>\nThe local structure <CODE>CONTEXTMENUINFO<\/CODE> contains\ninformation about each of the context menus that are part of\nour composite.  We need to have the context menu pointer itself,\nas well as the number of menu identifiers consumed by that\ncontext menu by its\n<CODE><A HREF=\"http:\/\/msdn.microsoft.com\/library\/en-us\/shellcc\/platform\/shell\/reference\/ifaces\/icontextmenu\/QueryContextMenu.asp\">IContextMenu::QueryContextMenu<\/A><\/CODE> handler.\nWe&#8217;ll see why as we implement this class.\n<\/P>\n<PRE>\nHRESULT CCompositeContextMenu::Initialize(\n    IContextMenu **rgpcm, UINT cpcm)\n{\n  m_rgcmi = new CONTEXTMENUINFO[cpcm];\n  if (!m_rgcmi) {\n    return E_OUTOFMEMORY;\n  }<\/p>\n<p>  m_ccmi = cpcm;\n  for (UINT icmi = 0; icmi &lt; m_ccmi; icmi++) {\n    CONTEXTMENUINFO *pcmi = &amp;m_rgcmi[icmi];\n    pcmi-&gt;pcm = rgpcm[icmi];\n    pcmi-&gt;pcm-&gt;AddRef();\n    pcmi-&gt;cids = 0;\n  }<\/p>\n<p>  return S_OK;\n}\n<\/PRE>\n<P>\nSince a C++ constructor cannot fail, there are various\nconventions for how one handles failure during construction.\nOne convention, which I use here, is to put the bulk of\nthe work in an <CODE>Initialize<\/CODE> method, which can\nreturn an appropriate error code if the initialization fails.\n<\/P>\n<P>\n(Note that here I am assuming a non-throwing <CODE>new<\/CODE> operator.)\n<\/P>\n<P>\nOur initialization function allocates a bunch of\n<CODE>CONTEXTMENUINFO<\/CODE> structures and copies the\n<CODE>IContextMenu<\/CODE> pointers (and <CODE>AddRef<\/CODE>s them)\nfor safekeeping.  (Note that the <CODE>m_ccmi<\/CODE> member is\nnot set until after we know that the memory allocation succeeded.)\n<\/P>\n<P>\nThe destructor therefore undoes these operations.\n<\/P>\n<PRE>\nCCompositeContextMenu::~CCompositeContextMenu()\n{\n  for (UINT icmi = 0; icmi &lt; m_ccmi; icmi++) {\n    m_rgcmi[icmi].pcm-&gt;Release();\n  }\n  delete[] m_rgcmi;\n}\n<\/PRE>\n<P>\n(If you don&#8217;t understand the significance of the <CODE>[]<\/CODE>,\n<A HREF=\"\/oldnewthing\/archive\/2004\/02\/03\/66660.aspx\">\nhere&#8217;s a refresher<\/A>.)\n<\/P>\n<P>\nThe <CODE>Create<\/CODE> pattern you saw last time, so this\nshouldn&#8217;t be too surprising.\n<\/P>\n<PRE>\nHRESULT CCompositeContextMenu::Create(IContextMenu **rgpcm, UINT cpcm,\n                                      REFIID riid, void **ppv)\n{\n  *ppv = NULL;<\/p>\n<p>  HRESULT hr;\n  CCompositeContextMenu *self = new CCompositeContextMenu();\n  if (self) {\n    if (SUCCEEDED(hr = self-&gt;Initialize(rgpcm, cpcm)) &amp;&amp;\n        SUCCEEDED(hr = self-&gt;QueryInterface(riid, ppv))) {\n      \/\/ success\n    }\n    self-&gt;Release();\n  } else {\n    hr = E_OUTOFMEMORY;\n  }\n  return hr;\n}\n<\/PRE>\n<P>\nAnd then the standard COM bookkeeping.\n<\/P>\n<PRE>\nHRESULT CCompositeContextMenu::QueryInterface(REFIID riid, void **ppv)\n{\n  IUnknown *punk = NULL;\n  if (riid == IID_IUnknown) {\n    punk = static_cast&lt;IUnknown*&gt;(this);\n  } else if (riid == IID_IContextMenu) {\n    punk = static_cast&lt;IContextMenu*&gt;(this);\n  } else if (riid == IID_IContextMenu2) {\n    punk = static_cast&lt;IContextMenu2*&gt;(this);\n  } else if (riid == IID_IContextMenu3) {\n    punk = static_cast&lt;IContextMenu3*&gt;(this);\n  }<\/p>\n<p>  *ppv = punk;\n  if (punk) {\n    punk-&gt;AddRef();\n    return S_OK;\n  } else {\n    return E_NOINTERFACE;\n  }\n}<\/p>\n<p>ULONG CCompositeContextMenu::AddRef()\n{\n  return ++m_cRef;\n}<\/p>\n<p>ULONG CCompositeContextMenu::Release()\n{\n  ULONG cRef = &#8211;m_cRef;\n  if (cRef == 0) delete this;\n  return cRef;\n}\n<\/PRE>\n<P>\nNow we reach our first interesting method:\n<CODE>IContextMenu::QueryContextMenu<\/CODE>:\n<PRE>\nHRESULT CCompositeContextMenu::QueryContextMenu(\n    HMENU hmenu, UINT indexMenu, UINT idCmdFirst,\n    UINT idCmdLast, UINT uFlags)\n{\n  UINT idCmdFirstOrig = idCmdFirst;\n  UINT cids = 0;<\/p>\n<p>  for (UINT icmi = 0; icmi &lt; m_ccmi; icmi++) {\n    CONTEXTMENUINFO *pcmi = &amp;m_rgcmi[icmi];\n    HRESULT hr = pcmi-&gt;pcm-&gt;QueryContextMenu(hmenu,\n                    indexMenu, idCmdFirst, idCmdLast, uFlags);\n    if (SUCCEEDED(hr)) {\n      pcmi-&gt;cids = (USHORT)hr;\n      cids += pcmi-&gt;cids;\n      idCmdFirst += pcmi-&gt;cids;\n    }\n  }<\/p>\n<p>  return MAKE_HRESULT(SEVERITY_SUCCESS, 0, cids);\n}\n<\/PRE>\n<P>\nWe ask each contained context menu\nin turn to add its commands to the context menu.  Here is\nwhere you see one of the reasons for the return value of the\n<CODE>IContextMenu::QueryContextMenu<\/CODE> method.\nBy telling tells the container how many menu identifiers\nyou used, the container knows how many are left for others.\nThe container then returns the total number of menu identifiers\nconsumed by all of the context menus.\n<\/P>\n<P>\nAnother reason for the return value of the\n<CODE>IContextMenu::QueryContextMenu<\/CODE> method\nis seen in the next helper method:\n<\/P>\n<PRE>\nHRESULT CCompositeContextMenu::ReduceOrdinal(\n    UINT_PTR *pidCmd, CONTEXTMENUINFO **ppcmi)\n{\n  for (UINT icmi = 0; icmi &lt; m_ccmi; icmi++) {\n    CONTEXTMENUINFO *pcmi = &amp;m_rgcmi[icmi];\n    if (*pidCmd &lt; pcmi-&gt;cids) {\n      *ppcmi = pcmi;\n      return S_OK;\n    }\n    *pidCmd -= pcmi-&gt;cids;\n  }\n  return E_INVALIDARG;\n}\n<\/PRE>\n<P>\nThis method takes a menu offset and figures out which\nof the contained context menus it belongs to,\nusing the return value from\n<CODE>IContextMenu::QueryContextMenu<\/CODE> to decide\nhow to divide up the identifier space.\nThe <CODE>pidCmd<\/CODE> parameter is in\/out.\nOn entry, it&#8217;s the menu offset for the composite\ncontext menu; on exit, it&#8217;s the menu offset for\nthe contained context menu that is returned via\nthe <CODE>ppcmi<\/CODE> parameter.\n<\/P>\n<P>\nThe <CODE>IContextMenu::InvokeCommand<\/CODE> is probably\nthe most complicated, since it needs to support the\nfour different ways of dispatching the command.\n<\/P>\n<PRE>\nHRESULT CCompositeContextMenu::InvokeCommand(\n                            LPCMINVOKECOMMANDINFO lpici) {<\/p>\n<p>  CMINVOKECOMMANDINFOEX* lpicix =\n                reinterpret_cast&lt;CMINVOKECOMMANDINFOEX*&gt;(lpici);\n  BOOL fUnicode = lpici-&gt;cbSize &gt;= sizeof(CMINVOKECOMMANDINFOEX) &amp;&amp;\n                  (lpici-&gt;fMask &amp; CMIC_MASK_UNICODE);\n  UINT_PTR idCmd = fUnicode ? reinterpret_cast&lt;UINT_PTR&gt;(lpicix-&gt;lpVerbW)\n                            : reinterpret_cast&lt;UINT_PTR&gt;(lpici-&gt;lpVerb);<\/p>\n<p>  if (!IS_INTRESOURCE(idCmd)) {\n    for (UINT icmi = 0; icmi &lt; m_ccmi; icmi++) {\n      HRESULT hr = m_rgcmi-&gt;pcm-&gt;InvokeCommand(lpici);\n      if (SUCCEEDED(hr)) {\n        return hr;\n      }\n    }\n    return E_INVALIDARG;\n  }<\/p>\n<p>  CONTEXTMENUINFO *pcmi;\n  HRESULT hr = ReduceOrdinal(&amp;idCmd, &amp;pcmi);\n  if (FAILED(hr)) {\n      return hr;\n  }<\/p>\n<p>  LPCWSTR pszVerbWFake;\n  LPCWSTR *ppszVerbW = fUnicode ? &amp;lpicix-&gt;lpVerbW : &amp;pszVerbWFake;\n  LPCSTR pszVerbOrig = lpici-&gt;lpVerb;\n  LPCWSTR pszVerbWOrig = *ppszVerbW;<\/p>\n<p>  lpici-&gt;lpVerb = reinterpret_cast&lt;LPCSTR&gt;(idCmd);\n  *ppszVerbW = reinterpret_cast&lt;LPCWSTR&gt;(idCmd);<\/p>\n<p>  hr = pcmi-&gt;pcm-&gt;InvokeCommand(lpici);<\/p>\n<p>  lpici-&gt;lpVerb = pszVerbOrig;\n  *ppszVerbW = pszVerbWOrig;<\/p>\n<p>  return hr;\n}\n<\/PRE>\n<P>\nAfter some preliminary munging to find the command identifier,\nwe dispatch the invocation in three steps.\n<\/P>\n<P>\nFirst, if the command is being dispatched as a string, then\nthis is the easiest case.  We loop through all the contained\ncontext menus asking each one if it recognizes the command.\nOnce one does, we are done.  And if nobody does, then we\nshrug and say we don&#8217;t know either.\n<\/P>\n<P>\nSecond, if the command being dispatched is an ordinal,\nwe ask <CODE>ReduceOrdinal<\/CODE> to figure out which contained\ncontext menu handler it belongs to.\n<\/P>\n<P>\nThird, we rewrite the <CODE>CMINVOKECOMMANDINFO<\/CODE> structure\nso it is suitable for use by the contained context menu handler.\nThis means changing the <CODE>lpVerb<\/CODE> member and possibly the\n<CODE>lpVerbW<\/CODE> member to contain the new menu offset relative\nto the contained context menu handler rather than being relative\nto the container.\nThis is complicated slightly by the fact that the Unicode verb\n<CODE>lpVerbW<\/CODE> might not exist.  We hide that behind a\n<CODE>pszVerbWFake<\/CODE> local variable which stands in if\nthere is no genuine <CODE>lpVerbW<\/CODE>.\n<\/P>\n<P>\nOkay, now that you see the basic idea behind distributing the\nmethod calls to the appropriate contained context menu, the\nrest should be comparatively easy.\n<\/P>\n<PRE>\nHRESULT CCompositeContextMenu::GetCommandString(\n                            UINT_PTR    idCmd,\n                            UINT        uType,\n                            UINT      * pwReserved,\n                            LPSTR       pszName,\n                            UINT        cchMax)\n{\n  HRESULT hr;\n  if (!IS_INTRESOURCE(idCmd)) {\n    for (UINT icmi = 0; icmi &lt; m_ccmi; icmi++) {\n      hr = m_rgcmi[icmi].pcm-&gt;GetCommandString(idCmd,\n                        uType, pwReserved, pszName, cchMax);\n      if (hr == S_OK) {\n        return hr;\n      }\n    }\n    if (uType == GCS_VALIDATEA || uType == GCS_VALIDATEW) {\n      return S_FALSE;\n    }\n    return E_INVALIDARG;\n  }<\/p>\n<p>  CONTEXTMENUINFO *pcmi;\n  if (FAILED(hr = ReduceOrdinal(&amp;idCmd, &amp;pcmi))) {\n    return hr;\n  }<\/p>\n<p>  return pcmi-&gt;pcm-&gt;GetCommandString(idCmd, uType,\n                        pwReserved, pszName, cchMax);\n}\n<\/PRE>\n<P>\nThe <CODE>GetCommandString<\/CODE> method follows the same\nthree-step pattern as <CODE>InvokeCommand<\/CODE>.\n<\/P>\n<P>\nFirst, dispatch any string-based commands by calling each\ncontained context menu handler until somebody accepts it.\nIf nobody does, then reject the command.\n(Note the special handling of <CODE>GCS_VALIDATE<\/CODE>,\nwhich expects <CODE>S_FALSE<\/CODE> rather than an error code.)\n<\/P>\n<P>\nSecond, if the command is specified by ordinal, ask\n<CODE>ReduceOrdinal<\/CODE> to figure out which contained\ncontext menu handler it belongs to.\n<\/P>\n<P>\nThird, pass the reduced command to the applicable contained\ncontext menu handler.\n<\/P>\n<P>\nThe last methods are made easier by a little helper function:\n<\/P>\n<PRE>\nHRESULT IContextMenu_HandleMenuMsg2(\n            IContextMenu *pcm, UINT uMsg, WPARAM wParam,\n            LPARAM lParam, LRESULT* plResult)\n{\n  IContextMenu2 *pcm2;\n  IContextMenu3 *pcm3;\n  HRESULT hr;\n  if (SUCCEEDED(hr = pcm-&gt;QueryInterface(\n                    IID_IContextMenu3, (void**)&amp;pcm3))) {\n    hr = pcm3-&gt;HandleMenuMsg2(uMsg, wParam, lParam, plResult);\n    pcm3-&gt;Release();\n  } else if (SUCCEEDED(hr = pcm-&gt;QueryInterface(\n                    IID_IContextMenu2, (void**)&amp;pcm2))) {\n    if (plResult) *plResult = 0;\n    hr = pcm2-&gt;HandleMenuMsg(uMsg, wParam, lParam);\n    pcm2-&gt;Release();\n  }\n  return hr;\n}\n<\/PRE>\n<P>\nThis helper function takes an <CODE>IContextMenu<\/CODE>\ninterface pointer and tries to invoke\n<CODE>IContextMenu3::HandleMenuMsg2<\/CODE>; if that fails,\nthen it tries <CODE>IContextMenu2::HandleMenuMsg<\/CODE>; and\nif that also fails, then it gives up.\n<\/P>\n<P>\nWith this helper function, the last two methods are a piece of cake.\n<\/P>\n<PRE>\nHRESULT CCompositeContextMenu::HandleMenuMsg(\n            UINT uMsg, WPARAM wParam, LPARAM lParam)\n{\n  LRESULT lres;   \/\/ thrown away\n  return HandleMenuMsg2(uMsg, wParam, lParam, &amp;lres);\n}\n<\/PRE>\n<P>\nThe <CODE>IContextMenu2::HandleMenuMsg<\/CODE> method is just\na forwarder to the <CODE>IContextMenu3::HandleMenuMsg2<\/CODE>\nmethod:\n<\/P>\n<PRE>\nHRESULT CCompositeContextMenu::HandleMenuMsg2(\n            UINT uMsg, WPARAM wParam, LPARAM lParam,\n            LRESULT* plResult)\n{\n  for (UINT icmi = 0; icmi &lt; m_ccmi; icmi++) {\n    HRESULT hr;\n    if (SUCCEEDED(hr = IContextMenu_HandleMenuMsg2(\n                    m_rgcmi[icmi].pcm, uMsg, wParam, lParam,\n                    plResult))) {\n      return hr;\n    }\n  }\n  return E_NOTIMPL;\n}\n<\/PRE>\n<P>\nAnd the <CODE>IContextMenu3::HandleMenuMsg2<\/CODE> method\nmerely walks through the list of context menu handlers,\nasking each one whether it wishes to handle the command,\nstopping when one finally does.\n<\/P>\n<P>\nArmed with this composite menu class, we can show it off\nin our sample program by compositing the &#8220;real&#8221; context menu\nwith our <CODE>CTopContextMenu<\/CODE>, thereby showing\nhow you can combine multiple context menus into one big\ncontext menu.\n<\/P>\n<PRE>\nHRESULT GetCompositeContextMenuForFile(HWND hwnd,\n            LPCWSTR pszPath, REFIID riid, void **ppv)\n{\n  *ppv = NULL;\n  HRESULT hr;<\/p>\n<p>  IContextMenu *rgpcm[2] = { 0 };\n  if (SUCCEEDED(hr = GetUIObjectOfFile(hwnd, pszPath,\n                        IID_IContextMenu, (void**)&amp;rgpcm[0])) &amp;&amp;\n      SUCCEEDED(hr = CTopContextMenu::Create(\n                        IID_IContextMenu, (void**)&amp;rgpcm[1])) &amp;&amp;\n      SUCCEEDED(hr = CCompositeContextMenu::Create(rgpcm, 2, riid, ppv))) {\n      \/\/ yay\n  }\n  if (rgpcm[0]) rgpcm[0]-&gt;Release();\n  if (rgpcm[1]) rgpcm[1]-&gt;Release();<\/p>\n<p>  return hr;\n}\n<\/PRE>\n<P>\nThis function builds the composite by creating the two\ncontained context menu handlers, then creating a composite\ncontext menu that contains both of them.  We can use this\nfunction by making the same one-line tweak to the\n<CODE>OnContextMenu<\/CODE> function that we tweaked last time:\n<\/P>\n<PRE>\nvoid OnContextMenu(HWND hwnd, HWND hwndContext, int xPos, int yPos)\n{\n  POINT pt = { xPos, yPos };\n  if (pt.x == -1 &amp;&amp; pt.y == -1) {\n    pt.x = pt.y = 0;\n    ClientToScreen(hwnd, &amp;pt);\n  }<\/p>\n<p>  IContextMenu *pcm;\n  if (SUCCEEDED(<FONT COLOR=\"blue\">GetCompositeContextMenuForFile(\n                    hwnd, L&#8221;C:\\\\Windows\\\\clock.avi&#8221;,\n                    IID_IContextMenu, (void**)&amp;pcm)<\/FONT>)) {\n    &#8230;\n<\/PRE>\n<P>\nNotice that with this composite context menu, the menu help text\nthat we update in our window title tracks across both the\noriginal file context menu and our &#8220;Top&#8221; context menu.\nCommands from either half are also invoked successfully.\n<\/P>\n<P>\nThe value of this approach over the method from\n<A HREF=\"\/oldnewthing\/archive\/2004\/10\/04\/237507.aspx\">\npart&nbsp;9<\/A>\nis that you no longer have to coordinate the customization of\nthe context menu between two pieces of code.  Under the previous\ntechnique, you had to make sure that the code that updated the\nmenu help text was in sync with the code that added the custom\ncommands.\n<\/P>\n<P>\nUnder the new method, all the customizations are kept in one\nplace (in the &#8220;Top&#8221; context menu which is inside the\ncomposite context menu), so that the window procedure doesn&#8217;t need\nto know what customizations have taken place.\nThis becomes more valuable if there are multiple points at which\ncontext menus are displayed, some uncustomized, others customized\nin different ways.  Centralizing the knowledge of the customizations\nsimplifies the design.\n<\/P>\n<P>\nOkay, I think that&#8217;s enough on context menus for now.\nI hope you&#8217;ve gotten a better understanding of how they work,\nhow you can exploit them, and most importantly, how you can\nperform meta-operations on them with techniques like composition.\n<\/P>\n<P>\nThere are still some\nother things you can do with context menus, but I&#8217;m going to leave you\nto experiment with them on your own.  For example, you can use\n<A HREF=\"http:\/\/msdn.microsoft.com\/library\/en-us\/shellcc\/platform\/shell\/reference\/ifaces\/icontextmenu\/getcommandstring.asp\">\nthe IContextMenu::GetCommandString method<\/A> to walk\nthe menu and obtain a language-independent command mame\nfor each item.\nThis is handy if you want to, say, remove the &#8220;delete&#8221; option:\nYou can look for the command whose language-independent name\nis &#8220;delete&#8221;.  This name does not change when the user changes\nlanguages; it will always be in English.\n<\/P>\n<P>\n<A HREF=\"\/oldnewthing\/archive\/2004\/09\/28\/235242.aspx\">\nAs we&#8217;ve noticed before<\/A>, you need to be aware\nthat many context menu handlers don&#8217;t implement\nthe IContextMenu::GetCommandString method fully,\nso there will likely be commands that you simply cannot get\na name for.  Them&#8217;s the breaks.\n<\/P>\n<P>\n[Editing errors corrected, 11am.]\n<\/P><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Okay, now that we have two context menu handlers we want to compose (namely, the &#8220;real&#8221; one from the shell namespace and a &#8220;fake&#8221; one that contains bonus commands we want to add), we can use merge them together by means of a composite context menu handler. The kernel of the composite context menu is [&hellip;]<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-37633","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Okay, now that we have two context menu handlers we want to compose (namely, the &#8220;real&#8221; one from the shell namespace and a &#8220;fake&#8221; one that contains bonus commands we want to add), we can use merge them together by means of a composite context menu handler. The kernel of the composite context menu is [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/37633","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=37633"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/37633\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=37633"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=37633"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=37633"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}