Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / Win32

Intertexti - Resurrecting HyperCard

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
2 Feb 2013CPOL20 min read 36.6K   1.5K   17   7
A prototype application for cross-referencing and indexing files, URL's.

Image 1

Contents

Important: To Run The Application... 

Download the WebKit.NET binaries from here and copy them into the bin\x86\Debug folder!

Introduction

Intertexti is an initiative I'm undertaking to implement something similar to Apple's HyperCard application.  The name is based on the Latin word for "intertwined", since the notecards in the system can be linked any-which-way to other cards. 

What You Will Encounter 

In this application, I take advantage of the following third party components:

as well as:

What Am I Trying To Accomplish ? 

The problem I'm trying to solve is this - in most everything that I do either for work or personal projects, I find a lot of useful information spread across web pages.  I need to organize those pages in better ways than just bookmarking -- I want to be able to add tags, create an index from those tags, see what pages I want to associate with other pages (which often enough the page itself doesn't link to directly), and so forth.

Furthermore, I want to be able to associate my own notes, todo lists, commentary, etc., and reference back to relevant pages.  Again, this kind of note taking and cross-referencing is not something a browser supports.  Furthermore, and especially with regards to my own notes, I want to be able to organize them relationally, which can often be expressed visually as a table of contents -- something that shows the structure and organization of my own notes as well as providing links to sources, further reading, and so forth.

 A Word document is simply to linear for me -- it's one dimensional, like the vertical scrollbar.  The second dimension, the cross-referencing of documents, is what Intertexti achieves, at least in prototype form.

My Design Goals

As usual, the goal is to design something that is flexible, almost immediately useable, and is implemented with minimal amount of code.  You will note my heavy reliance on my standard operating practice of using XML for everything declarative.  You will also note that, as a result of the MVC architecture, methods are very small -- some only a line or two.  Lastly, the underlying application organization should be support the idea of extensibility -- for example, if you don't want to use WebKit is the browser engine, you should be able to replace it with something else with a minimal amount of fuss.

About This Article

I like to write the article as I'm doing the coding, so what you will encounter here is a log, if you will, of the development process.  The final source code has slight differences from the code presented here--for example, the final version of the NotecardRecord class uses a factory pattern rather than a publicly exposed constructor.  But the idea is that the reader will get a sense of how the application was developed and the problems I encountered (like handling right-click mouse events on the WebKit browser).

Initial Features

My initial features are not too ambitious:

  • Dockable windows using Weifen Luo's Dock Panel Suite. 
  • A side panel for the table of contents 
  • A side panel for the index
  • A side panel for links to other notecards--"references".  One of the usability issues I keep encountering is that links are usually embedded - portions of text, areas on an image, etc.  For text, this means having to scan the text for links.  What I want instead is for all the possible links to be displayed in a separate section with annotation describing the link. 
  • A side panel for notecards that link to this notecard--"referenced by".  Often enough, I want to see what the broader theme is.
  • An HTML-based notecard, allowing any HTML content and possible future scripting.

As might be surmised by the description above of references, I view references as being unidirectional, essentially drilling down (or at least horizontally) whereas the links for "referenced by" are popping up the tree.

Visualizing the Navigation Concept

There are four forms of navigation, but these should be intuitive to the user:

  1. Table of Contents: Each notecard has a title and the table of contents is generated from title information, and sub-sections are determined by references on the notecard designated as "subsection" or something similar.  If a reference points to a notecard already in the table of contents, it is ignored.
  2. Index: This lists all the notecards where a specific text tag is used.
  3. Forward references: Not every reference needs to go into the table of contents, but a link to another notecard might still be useful.  These (in addition to the table of contents references) are displayed in the references navigation.
  4. Reverse references: This displays the list of notecards referencing the current notecard.

For example, given 5 notecards:

Image 2

The four navigable components are:

Image 3

Initial User Interface

UI Scaffold

The above pieces can be quickly put together in a scaffold--no content, just the layout of the views:

Image 4

The Imperative Code

Getting this scaffolding up and running takes about 200 lines of code, leveraging Weifen Luo's DockPanelSuite and my recent article on Decoupling Content from Container,  A basic MVC model is used.

Program.cs

Here we simply instantiate the application's main form:

static class Program
{
  [STAThread]
  public static void Main()
  {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Form form = Instantiate<Form>("mainform.xml", null);
    Application.Run(form);
  }

  public static T Instantiate<T>(string filename, Action<MycroParser> AddInstances)
  {
    MycroParser mp = new MycroParser();

    if (AddInstances != null)
    {
      AddInstances(mp);
    }

    XmlDocument doc = new XmlDocument();
    doc.Load(filename);
    mp.Load(doc, "Form", null);
    T obj = (T)mp.Process();

    return obj;
  }
}

ApplicationFormView.cs

The application's view is simply a placeholder for the DockPanel instance, as this is referenced by the controller to respond to menu events:

public class ApplicationFormView : Form
{
  public DockPanel DockPanel { get; protected set; }

  public ApplicationFormView()
  {
  }
}

ApplicationFormController.cs

The controller handles all of the application's main form events.  Most of this is boilerplate for working with DockPanelSuite.  The only noteworthy thing here is to observe that the controller is derived from the abstract class ViewController, which requires defining the concrete view type to which the controller is associated.  This makes it easier in the controller to work with the specific view properties -- we can avoid all the casting that would otherwise be required.

public class ApplicationFormController : ViewController<ApplicationFormView>
{
  public ApplicationFormController()
  {
  }

  protected void Exit(object sender, EventArgs args)
  {
    View.Close();
  }

  protected void Closing(object sender, CancelEventArgs args)
  {
    SaveLayout();
  }

  protected void RestoreLayout(object sender, EventArgs args)
  {
    CloseAllDockContent();
    LoadTheLayout("defaultLayout.xml");
  }

  protected void LoadLayout(object sender, EventArgs args)
  {
    if (File.Exists("layout.xml"))
    {
      LoadTheLayout("layout.xml");
    }
    else
    {
      RestoreLayout(sender, args);
    }
  }

  protected void LoadTheLayout(string layoutFilename)
  {
    View.DockPanel.LoadFromXml(layoutFilename, ((string persistString)=>
    {
      string typeName = persistString.LeftOf(',').Trim();
      string contentMetadata = persistString.RightOf(',').Trim();
      IDockContent container = InstantiateContainer(typeName, contentMetadata);
      InstantiateContent(container, contentMetadata);

      return container;
    }));
  }

  protected void SaveLayout()
  {
    View.DockPanel.SaveAsXml("layout.xml");
  }

  protected IDockContent InstantiateContainer(string typeName, string metadata)
  {
    IDockContent container = null;

    if (typeName == typeof(GenericPane).ToString())
    {
      container = new GenericPane(metadata);
    }
    else if (typeName == typeof(GenericDocument).ToString())
    {
      container = new GenericDocument(metadata);
    }

  return container;
  }

  protected void InstantiateContent(object container, string filename)
  {
    Program.Instantiate<object>(filename, ((MycroParser mp) => { mp.AddInstance("Container", container); }));
  }

  protected void NewDocument(string filename)
  {
    GenericDocument doc = new GenericDocument(filename);
    InstantiateContent(doc, filename);
    doc.Show(View.DockPanel);
  }

  protected void NewPane(string filename)
  {
    GenericPane pane = new GenericPane(filename);
    InstantiateContent(pane, filename);
    pane.Show(View.DockPanel);
  }

  protected void CloseAllDockContent()
  {
    if (View.DockPanel.DocumentStyle == DocumentStyle.SystemMdi)
    {
      foreach (Form form in View.MdiChildren)
      {
        form.Close();
      }
    }
    else
    {
      for (int index = View.DockPanel.Contents.Count - 1; index >= 0; index--)
      {
        if (View.DockPanel.Contents[index] is IDockContent)
        {
          IDockContent content = (IDockContent)View.DockPanel.Contents[index];
          content.DockHandler.Close();
        }
      }
    }
  }
} 

ViewController.cs

All controllers associated with views are derived from ViewController, which simply provides a typed instance of the underlying view, accessible in the derived controller class:

public abstract class ViewController<T> : ISupportInitialize
{
  public T View { get; set; }

  public ViewController()
  {
  }
  
  public virtual void BeginInit()
  {
  }

  public virtual void EndInit()
  {
  }
}

We will see later where the EndInit() virtual method is used, but for now, just keep in mind that the instantiation engine calls this method when the object has been completely instantiated, which we can take advantage of to do some application-specific initialization in the controller.

The Declarative Code

The definition of the layout and initial settings is handled by the declarative code, instantiated using MycroXaml (with some modifcations).

mainform.xml

This defines the layout of the application.  Note here how various assemblies are being pulled in and the view and controller is being instantiated with final property and event wire-up done last.

<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:ixc="Intertexti.Controllers, Intertexti"
  xmlns:ixv="Intertexti.Views, Intertexti"
  xmlns:wfui="WeifenLuo.WinFormsUI.Docking, WeifenLuo.WinFormsUI.Docking"
  xmlns:def="def"
  xmlns:ref="ref">

  <ixv:ApplicationFormView def:Name="applicationFormView" Text="Intertexti" Size="800, 600" IsMdiContainer="true">
  <ixc:ApplicationFormController def:Name="controller" View="{applicationFormView}"/>
  <ixv:Controls>
    <wfui:DockPanel def:Name="dockPanel" Dock="Fill"/>
      <wf:MenuStrip>
        <wf:Items>
          <wf:ToolStripMenuItem Text="&amp;File">
            <wf:DropDownItems>
              <wf:ToolStripMenuItem Text="E&amp;xit" Click="{controller.Exit}"/>
            </wf:DropDownItems>
          </wf:ToolStripMenuItem>
          <wf:ToolStripMenuItem Text="&amp;Window">
            <wf:DropDownItems>
              <wf:ToolStripMenuItem Text="Restore &amp;Layout" Click="{controller.RestoreLayout}"/>
            </wf:DropDownItems>
          </wf:ToolStripMenuItem>
        </wf:Items>
      </wf:MenuStrip>
    </ixv:Controls>
  <!-- Forward references -->
  <!-- Form events requiring the controller must be wired after the controller and form have been instantiated. -->
  <ixv:ApplicationFormView ref:Name="applicationFormView" DockPanel="{dockPanel}" Load="{controller.LoadLayout}" Closing="{controller.Closing}"/>
  </ixv:ApplicationFormView>
</MycroXaml>

The Four Panes

There are four initial panes defined in:

  • indexPane.xml
  • linksToPane.xml
  • referencedByPane.xml
  • tableOfContentsPane.xml

and they all are very similar, so I'll show the markup for only one of them, indexPane.xml:

<?xml version="1.0" encoding="utf-8" ?>
<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:ix="Intertexti, Intertexti"
  xmlns:ref="ref">
  <ix:GenericPane ref:Name="Container"
    TabText="Index"
    ClientSize="400, 190"
    BackColor="White"
    ShowHint="DockLeft">
    <ix:Controls>
      <wf:TreeView Dock="Fill">
        <wf:Nodes>
          <wf:TreeNode Text="Index">
          </wf:TreeNode>
        </wf:Nodes>
      </wf:TreeView>
    </ix:Controls>
  </ix:GenericPane>
</MycroXaml>

The only thing at the moment that differs between the panes is the text and the ShowHint property value assignment.

Notecards as a Browser Control

Image 5

It seemed most reasonable to leverage web browser technology as a means for rendering notecards.  This also has the future possibility of embedding scripts or other control logic to create more sophisticated and dynamic notecards.  Rather than use the browser that is embedded in .NET, I decided to use WebKit, specifically the WebKit.NET implementation.  You should be aware however that this implementation does not look like it's actively supported (the last update was August 2010) and is a wrapper for WebKit,  Readers should consider looking at open-webkit-sharp instead.  For the moment, the stalled WebKit.NET project is sufficient for this article for two reasons: I'm using VS2008 and open-webkit-sharp's binaries are built with VS2010/12, and I also can't get the a simple browser application to work and I don't want to spend the time fussing with it right now.

Imperative Code

The imperative code is simply stubs to get something basic working.

NotecardController.cs

There is no implementation:

public class NotecardController :ViewController<NotecardView>
{
}

NotecardView.cs

The only implementation here is to load something into the browser window:

public class NotecardView : UserControl
{
  protected WebKitBrowser browser;

  public WebKitBrowser Browser
  {
    get { return browser; }
    set { browser = value; browser.Navigate("http://www.codeproject.com"); }
  }

  public NotecardView()
  {
  }
}

ApplicationFormController.cs

A method has been added to handle the menu event to add a new notecard:

protected void NewNotecard(object sender, EventArgs args)
{
  NewDocument("notecard.xml");
}

Declarative Code

The notecard shown in the screenshot above is instantiated by notecard.xml.

notecard.xml

<?xml version="1.0" encoding="utf-8" ?>
<MycroXaml Name="Form"
  xmlns:ixc="Intertexti.Controllers, Intertexti"
  xmlns:ixv="Intertexti.Views, Intertexti"
  xmlns:ix="Intertexti, Intertexti"
  xmlns:wk="WebKit, WebKitBrowser"
  xmlns:def="def"
  xmlns:ref="ref">
  <ix:GenericDocument ref:Name="Container" Text="Notecard">
    <ix:Controls>
      <ixv:NotecardView def:Name="notecardView" Dock="Fill">
        <ixv:Controls>
          <wk:WebKitBrowser def:Name="browser" Dock="Fill"/>
        </ixv:Controls>
      </ixv:NotecardView>
    </ix:Controls>
    <ixv:NotecardView ref:Name="notecardView" Browser="{browser}"/>
    <ixc:NotecardController def:Name="notecardController" View="{notecardView}"/>
  </ix:GenericDocument>
</MycroXaml>

mainform.xml

A new menu item has been added:

<wf:ToolStripMenuItem Text="&amp;Notecard">
  <wf:DropDownItems>
    <wf:ToolStripMenuItem Text="&amp;New" Click="{controller.NewNotecard}"/>
  </wf:DropDownItems>
</wf:ToolStripMenuItem>

Tabbed Browsing

A lot of things that I want to organize are actually URL's, so I'm going to stick with the basic concept of navigating and linking together URL's.  Some of this will end up being a bit hokey because I'm putting off actually editing notecards until later in this article, but we can get the entire application behavior regarding linkages just by working with URL's.  And yes, this will end up creating a tabbed browser application with the ability to organize and associate web pages.  Amusing, isn't it?

What we need first is the ability to associate four things with a notecard:

  1. The desired URL
  2. A table of contents label
  3. Keywords, which will be used to generate the index information
  4. Linkage, allowing us to describe that a notecard is associated with another notecard

UI Changes

The first three items above (URL, TOC, and Keywords) are a standard part of each notecard.  The question becomes, should this information be associated with each notecard or in a location that is context-specific to the selected notecard?  I've opted for the second option, as this allows us to remove some of the clutter that we don't need to be looking at when simply navigating the data.  Ideally, this should be another DockPanel pane, giving the user the flexibility to move it around where they want:

Image 6

<?xml version="1.0" encoding="utf-8" ?>
<MycroXaml Name="Form"
  xmlns:ix="Intertexti, Intertexti"
  xmlns:ixctrl="Intertexti.Controls, Intertexti"
  xmlns:ixc="Intertexti.Controllers, Intertexti"
  xmlns:def="def"
  xmlns:ref="ref">
  <ix:GenericPane ref:Name="Container" TabText="Notecard Info" ClientSize="400, 190" BackColor="White" ShowHint="DockTop">
  <ixc:MetadataController def:Name="controller" AppController="{ApplicationFormController}"/>
    <ix:Controls>
      <ixctrl:LabeledTextBox LabelText="URL:" Location="5, 5" TextDataChanged="controller.NavigateToURL"/>
      <ixctrl:LabeledTextBox LabelText="TOC:" Location="5, 30"/>
      <ixctrl:LabeledTextBox LabelText="Tags:" Location="5, 55"/>
    </ix:Controls>
  </ix:GenericPane>
</MycroXaml>

Tracking the Active Notecard

In the notecard XML, I've added the line:

<ixa:RegisterDocumentController 
  App="{ApplicationFormController}" 
  Container="{Container}" 
  Controller="{controller}"/>

Note the markup element RegisterDocumentController.  This is a serious "cheat" on my part, providing the capability to add "actions" through the instantiation and post-initialization of instances.  The following diagram illustrates why we need this:

Image 7

The question is, how do we get the MetadataController to tell the NotecardController to navigate to a particular URL?  Furthermore, there can be multiple notecards displayed at the same time, so we need a way to track the active notecard.  DockPanelSuite provides an event for keeping track of the active notecard, which we wire up in the mainForm markup, as this is an event provided by DockPanel:

<wfui:DockPanel def:Name="dockPanel" Dock="Fill" ActiveDocumentChanged="{controller.ActiveDocumentChanged}"/>

Implemented in the ApplicationFormController as:

public IDocumentController ActiveDocumentController { get; protected set; }

protected void ActiveDocumentChanged(object sender, EventArgs args)
{
  DockPanel dockPanel = (DockPanel)sender;
  IDockContent content = dockPanel.ActiveDocument;
  ActiveDocumentController = documentControllerMap[content];
}

However, we still need to register the controller associated with the dock content, which is what the "action" RegisterDocumentController does:

public class RegisterDocumentController : DeclarativeAction
{
  public ApplicationFormController App { get; protected set; }
  public IDockContent Container { get; protected set; }
  public IDocumentController Controller { get; protected set; }

  public override void EndInit()
  {
    App.RegisterDocumentController(Container, Controller);
  }
}

And in the application controller:

public void RegisterDocumentController(IDockContent content, IDocumentController controller)
{
  documentControllerMap[content] = controller;
}

Now, the metadata controller can get the active notecard controller from the application controller -- three controllers are involved!

public class MetadataController
{
  public ApplicationFormController AppController { get; set; }

  public void NavigateToURL(string url)
  {
    ((INotecardController)AppController.ActiveDocumentController).NavigateToURL(url);
  }
}

Since we only have one kind of document, implemented as an INotecardController, we can safely cast the controller.  The above diagram now looks like this:

Image 8

Of course, we also need the ability to remove the entries in the content-controller map.  This is done by wiring up the DockPanel event:

<wfui:DockPanel 
  def:Name="dockPanel" 
  Dock="Fill" 
  ActiveDocumentChanged="{controller.ActiveDocumentChanged}" 
  ContentRemoved="{controller.ContentRemoved}"/>

and providing the implementation in the application controller:

protected void ContentRemoved(object sender, DockContentEventArgs e)
{
  documentControllerMap.Remove(e.Content);
}

One last little nuance -- by capturing the DocumentTitledChanged event of the Browser control:

<wk:WebKitBrowser 
  def:Name="browser" 
  Dock="Fill" 
  DocumentTitleChanged="{controller.DocumentTitleChanged}"/>

We can set the title of the tab:

protected void DocumentTitleChanged(object sender, EventArgs args)
{
  ((GenericDocument)View.Parent).Text = View.Browser.DocumentTitle;
}

We now have a non-persisting multi-tab browser:

Image 9

Persistence - The Model

It would behoove us to create and wire-up the model.  As you probably tell, I am not a model-driven-developer -- models tend to change a lot as the UI is being developed, so I like to get some of the UI aspects in place before constructing the model.  Then again, this approach works well when implementing on the fly with only a thin paper design.  If I were working with a fully storyboarded application, then yes, probably the model would be a good place to start, but still, it's less instant-gratifying.

In another radical approach, I'm not going to implement persistence with a third party database; a .NET DataSet is perfectly adequate for the job at hand -- persistable and relational.  Also, keep in mind that the layout of the UI (a model in its own right) is being handled completely by DockPanelSuite, so we don't need to worry about that. 

The model that we need at this point looks like this:

Image 10

See Appendix A for how the schema is declared and instantiated.

See Appendix B for the model persistence methods.

See Appendix C for the NotecardRecord implementation.

Appendices A-C are just boilerplate schema and data management code.  The more interesting model code is in the application specific features.  The model, in addition to maintaining the DataSet instance, also provides a property allowing a controller to get or set the active notecard record:

public NotecardRecord ActiveNotecardRecord { get; set; }

The active record is initialized when a new notecard is created:

public NotecardRecord NewNotecard()
{
  DataRow row = NewRow("Notecards"); 
  ActiveNotecardRecord = new NotecardRecord(row);

  return ActiveNotecardRecord;
}

protected DataRow NewRow(string tableName)
{
  DataRow row = dataSet.Tables["Notecards"].NewRow();
  dataSet.Tables["Notecards"].Rows.Add(row);

return row;
}

This occurs in the application's controller when a new notecard request (from the menu) is made:

protected void NewNotecard(object sender, EventArgs args)
{
  NewDocument("notecard.xml");
  NotecardRecord notecard = ApplicationModel.NewNotecard();
  notecard.IsOpen = true;
  ((NotecardController)ActiveDocumentController).SetNotecardRecord(notecard);
}

Furthermore, each controller maintains the notecard record instance to which it is associated.  Therefore, when the notecard in selected, the controller can update the metadata panel controls as well as the active record:

public void IsActive()
{
  // We may not have a record associated with the document!
  // This happens because DockPanelSuite opens documents as persisted in the layout.
  // TODO: Fix this, so documents are not persisted in the layout! We should always open with no documents!
  if (notecardRecord != null)
  {
    ApplicationModel.ActiveNotecardRecord = notecardRecord;
    ApplicationController.MetadataController.UpdateURL(notecardRecord.URL);
    ApplicationController.MetadataController.UpdateTOC(notecardRecord.TableOfContents);
    ApplicationController.MetadataController.UpdateTags(notecardRecord.Tags);
  }
}

When the metadata is updated, the metadata controller can update the active notecard record (the record's URL is updated actually in the notecard controller):

public void SetTableOfContents(string toc)
{
  ApplicationModel.ActiveNotecardRecord.TableOfContents = toc;
}

public void SetTags(string tags)
{
  ApplicationModel.ActiveNotecardRecord.Tags = tags;
}

Lastly, when a DataSet is loaded, a query is executed for all the notecards that were designated as "open" in the session associated with the DataSet:

public class ApplicationModel
{
  ...
  public List<NotecardRecord> GetOpenNotecards()
  {
    List<NotecardRecord> openNotecards = new List<NotecardRecord>();

    dataSet.Tables["Notecards"].AsEnumerable().
      Where(t => t.Field<bool>("IsOpen")).
      ForEach(t => openNotecards.Add(new NotecardRecord(t)));

    return openNotecards;
  }
  ...
}

The notecards that were open in the last session when the DataSet was saved are opened and directed to the appropriate URL's:

public class ApplicationFormController
{
  ...
  protected void OpenNotecardDocuments(List<NotecardRecord> notecards)
  {
    notecards.ForEach(t =>
    {
      NewDocument("notecard.xml");
      ((NotecardController)ActiveDocumentController).SetNotecardRecord(t);
      ((NotecardController)ActiveDocumentController).NavigateToURL(t.URL);
    });
  }
  ...
}

Again, because we only have one kind of document controller, we can safely cast the active document controller to the NotecardController type.

All of these events and interactions can be illustrated by the following diagram:

Image 11

Linking Notecards

For this prototype, I'm only going to implement linkages between notecards that are currently open (handling potentially thousands of notecards in the dataset is not exactly feasible at the moment.)  Wanting to implement this is a right-click operation on a notecard, I discovered that WebKit (the version I'm using, apparently this is fixed in SharpWebKit) doesn't allow me to set the ContextMenuStrip of the WebKitBrowser object, therefore I needed to implement the workaround described in Appendix D: Capturing Application-Wide Mouse Events.

Now that we have the right-click feature working correctly, we can create the context menu dynamically based on the open notecards (currently the text is set to the URL, we'll fix that later):

public class NotecardView : UserControl
{ 
  ...
  protected void CreateDynamicReferences()
  {
    ReferencesMenu.DropDownItems.Clear();
    ReferencedByMenu.DropDownItems.Clear();
    List<NotecardController> activeNotecardControllers = ApplicationController.ActiveNotecardControllers;

    activeNotecardControllers.ForEach(t =>
    {
      ToolStripMenuItem item1 = new ToolStripMenuItem(t.NotecardRecord.URL);
      item1.Tag = t;
      item1.Click += Controller.LinkReferences;
      ReferencesMenu.DropDownItems.Add(item1);

      ToolStripMenuItem item2 = new ToolStripMenuItem(t.NotecardRecord.URL);
      item2.Tag = t;
      item2.Click += Controller.LinkReferencedFrom;
      ReferencedByMenu.DropDownItems.Add(item2);
    });
  }
...
}

and the controller associated with the view handles the call to the model depending on the direction of the link:

public class NotecardController : ViewController<NotecardView>, IDocumentController, INotecardController
{
  ...
  public void LinkReferences(object sender, EventArgs e)
  {
    ToolStripMenuItem item = (ToolStripMenuItem)sender;
    NotecardController refController = (NotecardController)item.Tag;
    // Create an association between this controller, as the parent, and the refController, as the child.
    ApplicationModel.Associate(NotecardRecord, refController.NotecardRecord);
  }

  public void LinkReferencedFrom(object sender, EventArgs e)
  {
    ToolStripMenuItem item = (ToolStripMenuItem)sender;
    NotecardController refController = (NotecardController)item.Tag;
    // Create an association between this controller, as the child, and the refController, as the parent.
    ApplicationModel.Associate(refController.NotecardRecord, NotecardRecord);
  }
  ...
}

and finally the model handles the actual manipulation of the DataSet:

public class ApplicationModel
{
  ...
  public void Associate(NotecardRecord parent, NotecardRecord child)
  {
    DataRow row = dataSet.Tables["NotecardReferences"].NewRow();
    row["NotecardParentID"] = parent.ID;
    row["NotecardChildID"] = child.ID;
    dataSet.Tables["NotecardReferences"].Rows.Add(row);
  }
  ...
}

We also need to query the "references" and "referenced from" notecards, also implemented in the model.  Some of this code relies on Juan Francisco Morales Larios' excellent article on Linq Extended Joins.

public List<NotecardRecord> GetReferences()
{
  List<NotecardRecord> references = this["Notecards"].Join(this["NotecardReferences"].Where(t => t.Field<int>("NotecardParentID") == ActiveNotecardRecord.ID),
  pk => pk.Field<int>("ID"),
  fk => fk.Field<int>("NotecardChildID"),
  (pk, fk) => new NotecardRecord(pk)).ToList();

  return references;
}


public List<NotecardRecord> GetReferencedFrom()
{
  List<NotecardRecord> references = this["Notecards"].Join(this["NotecardReferences"].Where(t => t.Field<int>("NotecardChildID") == ActiveNotecardRecord.ID),
  pk => pk.Field<int>("ID"),
  fk => fk.Field<int>("NotecardParentID"),
  (pk, fk) => new NotecardRecord(pk)).ToList();

 return references;
}

For the table of contents, we also need the ability to get root notecards (those that aren't referenced by other notecards):

public List<NotecardRecord> GetRootNotecards()
{
  List<NotecardRecord> rootRecs = this["Notecards"].LeftExcludingJoin(
  this["NotecardReferences"], 
  pk => pk.Field<int>("ID"), 
  fk => fk.Field<int>("NotecardChildID"), 
  (pk, fk) => pk).Select(t => new NotecardRecord(t)).ToList();

  return rootRecs;
}

The References Views

We now have all the pieces to fill in the data in the TOC, indices, references, and referenced by panels.  Note that some of the markup illustrated earlier has changed -- I have now implemented controller and view classes for each of the panes.

The "links to" (aka references) and "referenced by" (aka referenced from) implementations are quite trivial.  Both views derive from:

public class ReferenceView : UserControl
{
  public ApplicationModel Model { get; protected set; }
  public TreeView TreeView { get; protected set; }

  public void UpdateTree(List<NotecardRecord> refs)
  {
    TreeView.Nodes.Clear();
    refs.ForEach(r =>
    {
      TreeNode node = new TreeNode(r.URL);
      node.Tag = r;
      TreeView.Nodes.Add(node);
    });
  }
}

where ReferencesView gets the references of the active record:

public class ReferencesView : ReferenceView 
{
  public void UpdateView()
  {
    List<NotecardRecord> refs = Model.GetReferences();
    UpdateTree(refs);
  }
}

as compared to ReferencesFromView, which gets the "references from" other notecards to the active record:

public class ReferencedFromView : ReferenceView
{
  public void UpdateView()
  {
    List<NotecardRecord> refs = Model.GetReferencedFrom();
    UpdateTree(refs);
  }
}

The Index View

This view accumulates and indexes the tags for each notecard and has the most code of any of the views:

public class IndexView : UserControl
{
  public ApplicationModel Model { get; protected set; }
  public TreeView TreeView { get; protected set; }

  public void RefreshView()
  {
    Dictionary<string, List<NotecardRecord>> tagRecordMap;

    TreeView.Nodes.Clear();
    tagRecordMap = BuildTagRecordMap();

    // Sort the list by tag value.
    var orderedIndexList = tagRecordMap.OrderBy((item)=>item.Key);

    BuildTree(orderedIndexList);
  }

  protected Dictionary<string, List<NotecardRecord>> BuildTagRecordMap()
  {
    Dictionary<string, List<NotecardRecord>> tagRecordMap = new Dictionary<string, List<NotecardRecord>>();

    // Build the view model, which is a list of references for tag item.
    Model.ForEachNotecard(rec =>
    {
      Model.GetTags(rec).Where(t=>!String.IsNullOrEmpty(t)).ForEach(t =>
      {
        List<NotecardRecord> records;

        if (!tagRecordMap.TryGetValue(t, out records))
        {
          records = new List<NotecardRecord>();
          tagRecordMap[t] = records;
        }

        records.Add(rec);
      });
    });

  return tagRecordMap;
  }

  protected void BuildTree(IOrderedEnumerable<KeyValuePair<string, List<NotecardRecord>>> orderedIndexList)
  {
    orderedIndexList.ForEach(item =>
    {
      TreeNode tn = new TreeNode(item.Key);
      TreeView.Nodes.Add(tn);

      if (item.Value.Count == 1)
      {
        // Only one notecard for this index item, so set the node's tag to the notecard record.
        tn.Tag = item.Value[0];
      }
      else if (item.Value.Count > 1)
      {
        // Multiple notecards for this index item, so create child nodes and set the node's tag to the associated notecard record.
        item.Value.ForEach(rec =>
        {
          TreeNode tn2 = new TreeNode(rec.URL);
          tn2.Tag = rec;
          tn.Nodes.Add(tn2);
        });
      }
    });
  }
}

Table of Contents View

The table of contents is built from root notecards (those that aren't referenced anywhere) and excludes any references that do not have a TOC entry, as well as ensuring we don't get into an infinite recursion state if there are circular references:

public class TableOfContentsView : UserControl
{
  public ApplicationModel Model { get; protected set; }
  public TreeView TreeView { get; protected set; }

  protected List<int> encounteredRecords;

  public void RefreshView()
  {
    encounteredRecords = new List<int>();
    List<NotecardRecord> rootRecs = Model.GetRootNotecards();
    TreeView.Nodes.Clear();
    PopulateTree(rootRecs);
  }

  protected void PopulateTree(List<NotecardRecord> rootRecs)
  {
    rootRecs.Where(r=>!String.IsNullOrEmpty(r.TableOfContents)).ForEach(r =>
    {
      encounteredRecords.Add(r.ID);
      TreeNode tn = new TreeNode(r.TableOfContents);
      tn.Tag = r;
      TreeView.Nodes.Add(tn);
      PopulateChildren(tn, r);
    });
  }

  protected void PopulateChildren(TreeNode node, NotecardRecord rec)
  {
    List<NotecardRecord> childRecs = Model.GetReferences(rec);

    childRecs.Where(r=>(!String.IsNullOrEmpty(r.TableOfContents)) && (!encounteredRecords.Contains(r.ID))).ForEach(r =>
    {
      encounteredRecords.Add(r.ID);
      TreeNode tn = new TreeNode(r.TableOfContents);
      tn.Tag = r;
      node.Nodes.Add(tn);
      // Recurse into grandchildren, etc.
      PopulateChildren(tn, r);
    });
  }
}

Using What We've Got

At this point, let's take a small breather and, using just URL's (both from the web and local files), put together some recipe notecards.  I want to organize my recipes by breakfast, lunch, and dinner, and I want the tags to be the ingredients so if I'm interested in a recipe that contains broccoli, I can find all those recipes.

First off, I created by hand some basic files (we'll implement a content editor later), that all look similar to this:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
  <head>
    <title>Recipes</title>
  </head>
  <body>
   <p>Recipes</p>
  </body>
</html>

These are going to be placeholders for my table of contents.  I add new notecards for each of my placeholders, generate the TOC, and we get a flat TOC:

Image 12

This isn't what we want - we want Recipes to references Breakfast, Lunch, and Dinner, so we select the Recipes notecard, and for each mealtime, right click and create the association:

Image 13

We now get a properly organized TOC and also note that the "Links To" pane shows what notecards the Recipes notecard references:

Image 14

Now we can add some recipes from the Internet, and after making the appropriate associations of a recipe to a mealtime, as well as populating the tags, we have the beginnings of a recipe book:

Image 15

The things to note here are:

  • the table of contents is a multi-level tree of all notecards that have a TOC value.
  • the "Links To" pane displays the notecard names that the current notecard ("Dinner") references.
  • the "Referenced By" pane displays the notecard names that reference the current nodecard ("Dinner").
  • the "Index" pane displays all the tags (our ingredients for all recipes) and if there is more than one notecard, shows the notecard options as sub-node items.

Handling TreeView Clicks

Of course, we want to be able to click on a TOC, index, or reference node and have it open the URL.  The event is wired up in the markup, for example:

<wf:TreeView def:Name="treeView" Dock="Fill" NodeMouseClick="{ApplicationFormController.OpenNotecard}">

and is routed to the application controller:

protected void OpenNotecard(object sender, TreeNodeMouseClickEventArgs args)
{
  NotecardRecord rec = args.Node.Tag as NotecardRecord;

  if (rec != null)
  {
    if (!rec.IsOpen)
    {
      NewDocument("notecard.xml");
      ((NotecardController)ActiveDocumentController).SetNotecardRecord(rec);
      ((NotecardController)ActiveDocumentController).NavigateToURL(rec.URL);
      ((NotecardController)ActiveDocumentController).IsActive();
      rec.IsOpen = true;
    }
    else
    {
      // Don't open a new document, select the one that is already open.
      IDockContent content = documentControllerMap.Single(t=>((NotecardController)t.Value).NotecardRecord==rec).Key;
      content.DockHandler.Show();
    }
  }
}

Editing Notecard Content

Lastly (at least for this prototype implementation) we want the user to be able to add actual content.  Since the notecard is based on a web browser, it would certainly make sense to use an HTML editor, and I found a decent one here.  Note that this is not intended to edit existing web pages - this for creating your own simple content (you do not want to use this for editing web pages from the Internet, among other things, all of the stylesheet information is lost.)

The HTML editor is added as a hidden control in the notepad view markup:

<editor:HtmlEditorControl def:Name="htmlEditor" Dock="Fill" Visible="false"/> 
<wk:WebKitBrowser def:Name="browser" Dock="Fill" .../>

and a right-click context menu option is added:

<wf:ToolStripMenuItem def:Name="editHtml" Text="&amp;Edit Document" Click="{controller.EditHtml}"/>

which the notepad controller handles (this is a bit kludgy right now):

protected void EditHtml(object sender, EventArgs args)
{
  if (!editing)
  {
    editing = true;
    View.BeginHtmlEditing();
  }
  else
  {
    editing = false;
    View.EndHtmlEditing();
    // Must get the new text from the HtmlEditor, as the WebKit Browser control
    // won't have updated the return value of the Browser.DocumentText property!
    NotecardRecord.HTML = View.HtmlEditor.InnerHtml;
  }
}

and the view does the rest:

public void BeginHtmlEditing()
{
  HtmlEditor.InnerHtml = Browser.DocumentText;
  Browser.Visible = false;
  HtmlEditor.Visible = true;
  EditHtml.Text = "&Save Html";
}

public void EndHtmlEditing()
{
  Browser.DocumentText = HtmlEditor.InnerHtml;
  HtmlEditor.Visible = false;
  Browser.Visible = true;
  EditHtml.Text = "&Edit Html";
}

Now the custom content simply needs to be handled correctly when we open a record, which means, instead of calling NavigateToURL, we're going to add the method ShowDocument and let the controller determine whether to use a URL or the custom HTML:

public void ShowDocument()
{
  if (!(String.IsNullOrEmpty(NotecardRecord.HTML)))
  {
    View.Browser.DocumentText = NotecardRecord.HTML;
  }
  else
  {
    NavigateToURL(NotecardRecord.URL);
  }
}

So, now by right-clicking on Edit Notecard:

Image 16

I can create my own notecards using a snazzy HTML editor:

Image 17

and I can now cross reference my custom notecards in the same why as with HTML files or URL's:

Image 18

Conclusion

At the end of the day (or the week, in this case, as it took about a week to put this all together) we end up with a usable prototype of my vision of resurrecting Apple's HyperCard concept. Maybe I'll be sued!  In any case, while this is a sufficiently usable application at this point, there are still a lot of rough edges--usability issues that need to be addressed (for example, you have to tell the program to regenerate the TOC and index from the View / Refresh menu) and probably lots of strange bugs.  But that work will be left for another day.  Also, there's some useful functionality missing, like being able to delete notecards!  You will also note that this is an x86 application because WebKit runs only in 32 bit mode.

As usual, if you're interested in contributing to this project, please let me know.  Personally, I'm interested in developing this into a viable commercial product, but the code presented here is available to the community.

Appendix A - Defining and Loading the Schema

The schema is represented as an object graph that instantiates a DataSet:

<?xml version="1.0" encoding="utf-8" ?>
<MycroXaml Name="Schema"
  xmlns:d="System.Data, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:def="def"
  xmlns:ref="ref">
  <d:DataSet Name="Dataset">
    <d:Tables>
      <d:DataTable Name="Notecards" TableName="Notecards">
        <d:Columns>
          <d:DataColumn Name="NotecardID" ColumnName="ID" AllowDBNull="false" AutoIncrement="true" DataType="System.Int32" />
          <d:DataColumn ColumnName="TableOfContents" AllowDBNull="true" DataType="System.String"/>
          <d:DataColumn ColumnName="URL" AllowDBNull="true" DataType="System.String"/>
          <d:DataColumn ColumnName="Title" AllowDBNull="true" DataType="System.String"/>
          <d:DataColumn ColumnName="HTML" AllowDBNull="true" DataType="System.String"/>
          <d:DataColumn ColumnName="IsOpen" AllowDBNull="true" DataType="System.Boolean"/>
        </d:Columns>
      </d:DataTable>
      <d:DataTable Name="NotecardReferences" TableName="NotecardReferences">
        <d:Columns>
          <d:DataColumn Name="NotecardReferenceID" ColumnName="ID" AllowDBNull="false" AutoIncrement="true" DataType="System.Int32"/>
          <d:DataColumn Name="NotecardParentID" ColumnName="NotecardParentID" AllowDBNull="false" DataType="System.Int32"/>
          <d:DataColumn Name="NotecardChildID" ColumnName="NotecardChildID" AllowDBNull="false" DataType="System.Int32"/>
        </d:Columns>
      </d:DataTable>
      <d:DataTable Name="Metadata" TableName="Metadata">
        <d:Columns>
          <d:DataColumn Name="MetadataID" ColumnName="ID" AllowDBNull="false" AutoIncrement="true" DataType="System.Int32"/>
          <d:DataColumn Name="Metadata_NotecardID" ColumnName="NotecardID" AllowDBNull="false" DataType="System.Int32"/>
          <d:DataColumn ColumnName="Tag" AllowDBNull="false" DataType="System.String"/>
        </d:Columns>
      </d:DataTable>
    </d:Tables>
    <d:Relations>
      <d:DataRelation Name="FK_Metadata_Notecard" ChildColumn="{Metadata_NotecardID}" ParentColumn="{NotecardID}"/>
      <d:DataRelation Name="FK_NotecardRef_Notecard1" ChildColumn="{NotecardParentID}" ParentColumn="{NotecardID}"/>
      <d:DataRelation Name="FK_NotecardRef_Notecard2" ChildColumn="{NotecardChildID}" ParentColumn="{NotecardID}"/>
    </d:Relations>
    <d:DataTable ref:Name="Notecards" PrimaryKey="{NotecardID}"/>
    <d:DataTable ref:Name="NotecardReferences" PrimaryKey="{NotecardReferenceID}"/>
    <d:DataTable ref:Name="Metadata" PrimaryKey="{MetadataID}"/>
  </d:DataSet>
</MycroXaml>

Unfortunately, certain properties (DataType) and classes (DataRelation) are not particularly friendly to declarative instantiation and need some "help":

public static class SchemaHelper
{
  public static DataSet CreateSchema()
  {
    MycroParser mp = new MycroParser();
    // Instantiation of schemas using .NET classes needs some help.
    mp.CustomAssignProperty += new CustomAssignPropertyDlgt(CustomAssignProperty);
    mp.InstantiateClass += new InstantiateClassDlgt(InstantiateClass);
    mp.UnknownProperty += new UnknownPropertyDlgt(UnknownProperty);
    XmlDocument doc = new XmlDocument();
    doc.Load("schema.xml");
    mp.Load(doc, "Schema", null);
    DataSet dataSet = (DataSet)mp.Process();

    return dataSet;
}

  public static void CustomAssignProperty(object sender, CustomPropertyEventArgs pea)
  {
    if (pea.PropertyInfo.Name == "DataType")
    {
      Type t = Type.GetType(pea.Value.ToString());
      pea.PropertyInfo.SetValue(pea.Source, t, null);
      pea.Handled = true;
    }
    else if (pea.PropertyInfo.Name == "PrimaryKey")
    {
      pea.PropertyInfo.SetValue(pea.Source, new DataColumn[] { (DataColumn)pea.Value }, null);
      pea.Handled = true;
    }
  }

  public static void InstantiateClass(object sender, ClassEventArgs cea)
  {
    MycroParser mp = (MycroParser)sender;

    if (cea.Type.Name == "DataRelation")
    {
      string name = cea.Node.Attributes["Name"].Value;
      string childColumnRef = cea.Node.Attributes["ChildColumn"].Value;
      string parentColumnRef = cea.Node.Attributes["ParentColumn"].Value;
      DataColumn dcChild = (DataColumn)mp.GetInstance(childColumnRef.Between('{', '}'));
      DataColumn dcParent = (DataColumn)mp.GetInstance(parentColumnRef.Between('{', '}'));
      cea.Result = new DataRelation(name, dcParent, dcChild);
      cea.Handled = true;
    }
  }

  public static void UnknownProperty(object sender, UnknownPropertyEventArgs pea)
  {
    // Ignore these attributes.
    // TODO: add the element name into the args, so we can also test the element for which we want to ignore certain properties.
    if ((pea.PropertyName == "ChildColumn") || (pea.PropertyName == "ParentColumn"))
    {
      pea.Handled = true;
    }
  }
}

Appendix B - Model Persistence Methods

This handles the saving and loading of the DataSet to an XML file:

public class ApplicationModel
{
protected DataSet dataSet;
protected string filename;

  public ApplicationModel()
  {
    dataSet = SchemaHelper.CreateSchema();
  }

  public void NewModel()
  {
    dataSet = SchemaHelper.CreateSchema();
    filename = String.Empty;
  }

  public void LoadModel(string filename)
  {
    this.filename = filename;
    dataSet = SchemaHelper.CreateSchema();
    dataSet.ReadXml(filename, XmlReadMode.IgnoreSchema);
  }

  public void SaveModel()
  {
    dataSet.WriteXml(filename, XmlWriteMode.WriteSchema);
  }

  public void SaveModelAs(string filename)
  {
    this.filename = filename;
    dataSet.WriteXml(filename, XmlWriteMode.WriteSchema);
  }
}

Appendix C: The NotecardRecord Implementation

This is a thin wrapper for the underlying DataRow associated with a notecard record:

public class NotecardRecord
{
  public string TableOfContents
  {
    get { return row.Field<string>("TableOfContents"); }
    set { row["TableOfContents"] = value; }
  }

  public string URL
  {
    get { return row.Field<string>("URL"); }
    set { row["URL"] = value; }
  }

  public string HTML
  {
    get { return row.Field<string>("HTML"); }
    set { row["HTML"] = value; }
  }

  public string Tags
  {
    get { return JoinTags(); }
    set { ParseTags(value); }
  }

  public bool IsOpen
  {
    get { return row.Field<bool>("IsOpen"); }
    set { row["IsOpen"] = value; }
  }

  protected DataRow row;

  public NotecardRecord(DataRow row)
  {
    this.row = row;
  }

  protected string JoinTags()
  {
    return String.Empty;
  }

  protected void ParseTags(string tags)
  {
  }
}

Appendix D: Capturing Application-Wide Mouse Events

This is a complicated workaround that requires first intercepting the application-wide right-click message and then posting (not sending) a custom message to the application to process the event, which in turn needs to make sure that the right-click is actually occurring on a noteacard.  Let's begin:

At startup, we register a custom window message and our custom message filter:

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern uint RegisterWindowMessage(string lpString);

public static void Main()
{
  RightClickWindowMessage = RegisterWindowMessage("IntertextiRightClick");
  IMessageFilter myFilter = new MyMessageFilter();
  Application.AddMessageFilter(myFilter);
  ...

The custom message filter looks for right-click events and posts a message to process the event.  The right-click is not filtered, allowing the application to handle it normally.  The reason we post the message is that we don't want to process it immediately -- we want to give Windows and the application the opportunity to do whatever it does, which, in our case, it to set focus to the control where the mouse was clicked (this is done for us somewhere).  Posting a message adds the message at the end of the Windows message queue, as opposed to SendMessage, which processes the message immediately if the two windows have the same thread.  The message filter:

public class MyMessageFilter : IMessageFilter
{
  [return: MarshalAs(UnmanagedType.Bool)]
  [DllImport("user32.dll", SetLastError = true)]
  static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

  public bool PreFilterMessage(ref Message m)
  {
    if (m.Msg == 0x204) //WM_RBUTTONDOWN
    {
      PostMessage(Program.MainForm.Handle, Program.RightClickWindowMessage, m.WParam, m.LParam);
    }

    return false; // do not filter
  }
}

Next, we look for our custom right-click message in the application's main form and fire an event:

public delegate void RightClickDlgt(int x, int y);
public class ApplicationFormView : Form
{
  public event RightClickDlgt RightClick;
  ...
  protected override void WndProc(ref Message m)
  {
    if (m.Msg == Program.RightClickWindowMessage)
    {
      // client area (x,y)
      int x = (int)(((ulong)m.LParam) & 0xFFFF);
      int y = (int)(((ulong)m.LParam) >> 16);

      if (RightClick != null)
      {
        RightClick(x, y);
      }
    }
    else
    {
      base.WndProc(ref m);
    }
  }
}

The event is wired up in the markup:

<ixv:ApplicationFormView 
  ref:Name="applicationFormView" 
  DockPanel="{dockPanel}" 
  Load="{controller.LoadLayout}" 
  Closing="{controller.Closing}" 
  RightClick="{controller.RightClick}"/>

and is handled by the application controller, which does nothing more than request that the active document controller show the context menu.

protected void RightClick(int x, int y)
{
  ActiveDocumentController.ShowContextMenu(x, y);
}

This request is passed to the controller's view (I don't expose the View property to other classes, so we always have to go through this step, because I don't want controllers to talk to views of other controllers):

public class NotecardController ...
{
  ...
  public void ShowContextMenu(int x, int y)
  {
    View.ShowContextMenu(new Point(x, y));
  }
...
}

And in the view, the coordinate is tested.  This requires converting the client coordinate of the application's active control to a screen coordinate, then comparing the screen coordinate with the screen coordinate of the notecard window, which for the moment is rather kludgy (the Parent.Parent.Parent thing):

public void ShowContextMenu(Point p)
{
  // Determine whether the mouse click occurred on the this control:

  // The point is relative to the control that currently has focus.
  ApplicationFormView app = (ApplicationFormView)Parent.Parent.Parent;
  Control activeCtrl = (Control)((ApplicationFormView)Parent.Parent.Parent).DockPanel.ActiveContent; // app.ActiveControl;

  // Convert to a screen point relative to the active control where the right-click occurred.
  Point screenPoint = activeCtrl.PointToScreen(p);

  // Get the screen location for this view.
  Point viewUpperLeft = PointToScreen(new Point(0, 0));
  Rectangle viewRect = new Rectangle(viewUpperLeft, Size);

  if (viewRect.Contains(screenPoint))
  {
    BrowserContextMenu.Show(PointToScreen(p));
  }
}

This is quite a bit of work to get the desired behavior, can probably be all ripped out if I were to use a different browser control, and needs cleanup because of the hard-coded dependency on the UI object graph.  However, it works, and that's what matters at the moment.

References

MycroXaml

DockPanelSuite

DockPanelSuite - Decoupling Content From Container

Linq Extended Joins

WinForms HTML Editor

WebKit.NET

open-webkit-sharp

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Espen Harlinn3-Feb-13 4:24
professionalEspen Harlinn3-Feb-13 4:24 
GeneralRe: My vote of 5 Pin
Marc Clifton3-Feb-13 6:39
mvaMarc Clifton3-Feb-13 6:39 
Thanks!

Marc

SuggestionCastaway? Pin
Brisingr Aerowing2-Feb-13 5:45
professionalBrisingr Aerowing2-Feb-13 5:45 
GeneralRe: Castaway? Pin
Marc Clifton2-Feb-13 12:04
mvaMarc Clifton2-Feb-13 12:04 
QuestionNice... And a question Pin
Brisingr Aerowing1-Feb-13 5:32
professionalBrisingr Aerowing1-Feb-13 5:32 
AnswerRe: Nice... And a question Pin
Marc Clifton1-Feb-13 8:25
mvaMarc Clifton1-Feb-13 8:25 
GeneralRe: Nice... And a question Pin
Brisingr Aerowing1-Feb-13 14:29
professionalBrisingr Aerowing1-Feb-13 14:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.