DynamoCore Modularization

## The Big Idea
The architecture of Dynamo was previously a tangled mess of dependencies. This PR refactors `DynamoCore` into a modular, layered architecture, where the boundaries of the various components of the application are clearly defined.

Or, in a nutshell, we are no longer accessing `DynamoModel` from remote parts of the application. This means that:
  1. In order to test parts of the application, we don't need to create an entire instance of the application.
  2. We can safely assume that different parts of the application aren't affecting other remote parts of the application unless there is a direct dependency between the two components.

## What's been changed?
* XML parsing
  - The logic for loading whole Workspaces, Nodes, Connectors, and Notes are now all located in a single place: `NodeGraph`
  - Loading .dyn and .dyf files now leverage this new API, previously we had duplicate (and unsynchronized) code for loading both.
* Node instantiation
  - `NodeFactory` has been overhauled; previously we were doing weird string parsing to determine how to create new nodes. Nothing was consistent, the API was cumbersome to use, and there was a ton of duplicate code.
  - All node instantiation is now processed through `NodeFactory`. Different types of nodes register `INodeLoader`  instances with the `NodeFactory`. For special kinds of nodes (Custom Nodes and Zero Touch Nodes), there are special `INodeLoaders` that handle the necessary extra logic for creating instances. This means that `NodeFactory` no longer needs to know about the `Function` and `DSFunction` classes.
  - All external implementations of Nodes which extend `NodeModel` are now instantiated using a pre-compiled `Expression`, as opposed to relying on an `Activator`. This means that instantiating large numbers of new nodes should be significantly faster (copy/paste, undo/redo, loading files).
* Custom Node Infrastructure
  - `CustomNodeManager` has become the primary interface for dealing with Custom Nodes.
    + It keeps track of `CustomNodeWorkspaces`, instead of just `CustomNodeDefinitions`.
  - `CustomNodeWorkspace` now emits events when the `CustomNodeDefinition` and `CustomNodeInfo` has changed.
    + This makes sense because the state of a `CustomNodeWorkspace` defines what the `CustomNodeDefinition` and `CustomNodeInfo` are.
    + These events are watched by `CustomNodeManager`, which then forwards them to `DynamoModel`, which then propagates them to the `EngineController`, Search Library, and Custom Node Instances.
  - Custom Node (`Function`) Instances are now created from `CustomNodeManager`, in order to ensure that they are kept in sync with their `CustomNodeDefinition`.
* Evaluation
  - Since `HomeWorkspaceModels` are the only kind of Workspaces that can be evaluated, `EngineController` has been moved to `HomeWorkspaceModel`
    + ~~Consequentially, all logic originally on `DynamoModel` associated with `EngineController` has now been moved to `HomeWorkspaceModel`~~ Unfortunately, `CodeBlockNodeModels` have a dependency on `EngineController` for Auto-complete. Further work can be done to decouple Auto-complete from `EngineController`, which would allow for total removal of `EngineController` from `DynamoModel`.
    + This sets the stage for eventually having multiple `HomeWorkspaceModels` active at once.
* Search
  - Search has been re-implemented to be as general as possible.
  - Search has been refactored to support the new method of instantiating Nodes.
  - The actual search method has been separated from the categorization infrastructure.
  - Categorization of Search Elements has been greatly generalized and simplified.

### Reviewers:
* [ ] @lukechurch 
* [ ] @ikeough 
* [ ] @Benglin 
* [ ] @pboyer 
This is as good a place as any to talk about the `ConnectorModel` rework.

`WorkspaceModel` no longer maintains its own collection of `ConnectorModel`. Instead, the are queried by traversing all of the nodes in the workspace, and accumulating their connectors. This removes tedious synchronization of a separate observable collection.

We do, however, still want to have notifications when connectors are added or removed. So now, when we call `ConnectorModel.Make`, the connector is created and attached to it's ports. The node that receives the connector as an input emits a `ConnectorAdded` event, which is then handled on the containing `WorkspaceModel`. The workspace then forwards this through its own `ConnectorAdded` event.

For removal, we have a method on `ConnectorModel` called `Delete`. This method disconnects the connector from the nodes it's attached to, and then fires the `Deleted` event on `ConnectorModel`. `WorkspaceModel` forwards this event as well.

This rework happened because `CodeBlockNodeModel` instances needed to be able to delete and create connectors from their workspace when the content of the code block changed.

Ideally, we would go in the opposite direction architecturally:  `WorkspaceModel` maintains a collection of `ConnectorModel` instances that don't have access to instances of the `NodeModel`s they're connector to. `NodeModel` instances also don't have access to their connectors. It is up to `WorkspaceModel` to handle things like setting the state of of a `NodeModel` when it's inputs change. In the case of the `CodeBlockNodeModel` needing the ability to create/delete connectors:  what they really need is to create/remove/move Ports. The workspace handles changes to ports by deleting/moving connectors. This is trivial if ports are given a unique identifier.