Skip to content

Simple DOM Editor WPF Programming Discussion

Gary edited this page Mar 10, 2015 · 23 revisions

Table of Contents

The ATF Simple DOM Editor WPF Sample is very similar to ATF Simple DOM Editor Sample, but it is implemented using ATF’s WPF framework. It shows defining a data model in an XML Schema, editing data, and saving the edited data in an XML file. Application data is displayed in two WPF ListView controls, and data properties can be examined in a property editor.

This application's data consist of sequences of events, which can contain resources. The sequence of events is displayed in the main tab, and the resources of the selected event are listed in the Resources control.

Note that "event" in this context means application data, not an event that is processed by an event handler. Both meanings of the term are used in this discussion, and the meaning of "event" should be clear from the context.

For information about WPF support in ATF, see WPF Support.

Programming Overview

The files EventSequenceView.xaml and ResourceListView.xaml define the EventSequenceView and ResourceListView controls. The contain the XAML and code behind for WPF controls that display the lists of events and resources associated with an event sequence. These also define behaviors and data bindings between the controls and application data.

SimpleDOMEditorWPF uses an XML Schema to define its data model of sequences of events with resources. Its schema loader also defines DOM adapters, palette items, and property descriptors that determine which properties appear in the property editors. The data model is identical to SimpleDOMEditor.

The application's PaletteClient, along with the WPF version of ATF's PaletteService, creates a palette of events and resource items.

SimpleDOMEditorWPF provides the DOM adapters Event, EventSequence, and Resource for their corresponding types to provide properties that access information in a DomNode of that type. These DOM adapters are nearly identical to ones in SimpleDOMEditor.

SimpleDOMEditorWPF relies heavily on contexts. ResourceListContext handles resources belonging to the selected event and manages the Resources ResourceListView editing. This context implements interfaces to handle displaying items, selection, and editing. EventSequenceContext does a similar job for a sequence of events in the main window's ListView.

EventSequenceDocument extends DomDocument, which implements IDocument. The Editor component is its document client, handling closing and saving documents.

The ResourceListEditor component registers a "slave" ListView control to display and edit resources that belong to the most recently selected event.

WPF and WinForms Commonalities

A lot of types have both WinForms and WPF versions. The WinForms versions are in-scope even for WPF applications, because their namespaces aren't segregated from the non-WinForms core GUI code. (That is, Atf.Gui.WinForms.dll's namespaces are the same as those in Atf.Gui.dll.) If you add a type to the MEF catalog and it doesn’t seem to be recognized, or you're seeing odd behavior at runtime such as typecasts that should succeed failing, make sure you're explicitly specifying the WPF version of the type.

For instance, the SchemaLoader class has the following statement to explicitly specify the WPF version:

using NodeTypePaletteItem = Sce.Atf.Wpf.Models.NodeTypePaletteItem;

Application Basics

SimpleDOMEditorWPF's initialization is typical for a WPF application, as described in WPF Application in the ATF Application Basics and Services section. In particular, SimpleDOMEditorWPF includes App.xaml and App.xaml.cs files. For more details, see App XAML. From the basic application described there, this sample builds up its functionality by overriding two methods in AtfApp: GetCatalog() and OnCompositionBeginning().

GetCatalog() builds on the simple example in App XAML by adding additional MEF components. Its TypeCatalog contains basic components in the application shell framework, such as SettingsService, FileDialogService, CommandService, and ControlHostService. The WPF MainWindow component handles the application's main window. SimpleDOMEditorWPF uses components to handle documents: DocumentRegistry, RecentDocumentCommands, StandardFileCommands, StandardFileExitCommand, and MainWindowTitleService. Editing and context management are handled by ContextRegistry, StandardEditCommands, and StandardEditHistoryCommands. How these common components function is discussed elsewhere in this Guide, such as Documents in ATF and ATF Contexts. Such components as SettingsService, RecentDocumentCommands, MainWindowTitleService can simply be added to the TypeCatalog, requiring no other modifications: they should "just work".

The PropertyEditor component handles property editing. This is well integrated with the ATF DOM, so the application only needs to include this component to be able to edit properties of the application's data. For details on property editing, see Property Editing in ATF.

Note that some of these components are the WPF versions rather than WinForms versions, such as ControlHostService, SettingsService, CommandService, FileDialogService, StandardEditCommands, PropertyEditor, and HelpCommands.

SimpleDOMEditorWPF adds a few custom components of its own:

  • PaletteClient: populate the palette with the basic DOM types, with help from the WPF ATF component PaletteService. For details, see Using a Palette.
  • Editor: create and save event sequence documents. See Document Handling.
  • SchemaLoader: load the XML Schema for the data model as well as define property descriptors and palette items. See Application Data Model.
  • EventSequenceContext: track event sequence contexts and controls that display event sequences. See Working With Contexts.
  • ResourceListContext: track resource contexts and controls that display resource lists. See Working With Contexts.
  • ResourceListEditor: display resources that belong to the most recently selected event. See ResourceListEditor Component.
Finally, the OnCompositionBeginning() method primarily adds Help capability:
protected override void OnCompositionBeginning()
{
	base.OnCompositionBeginning();

	var helpCommands = Container.GetExportedValueOrDefault<HelpCommands>();
	if (helpCommands != null)
	{
		// We don't have any context sensitive help, so disable these options.
		helpCommands.ShowContextHelp = false;
		helpCommands.EnableContextHelpUserSetting = false;

		// Set the URL for the sample app documentation.
		helpCommands.HelpFilePath = @"https://github.com/SonyWWS/ATF/wiki/ATF-Simple-DOM-Editor-WPF-Sample";
	}
}

HelpCommands is the ATF component for WPF that provides main application help command and context specific context menu help commands. Note that the HelpFilePath property is set to a URL. The result is that this URL is displayed in a Web browser when the user selects the menu item Help > Contents.

Display Controls

SimpleDOMEditorWPF defines two controls in XAML for lists of events and resources, EventSequenceView and ResourceListView, in the files EventSequenceView.xaml and ResourceListView.xaml. Both controls and their definitions are very similar.

EventSequenceView Control

The EventSequenceView control displays a sequence of events in the file being edited. EventSequenceView is a UserControl containing a Grid which contains a ListView, as seen in these lines from EventSequenceView.xaml that set up the control's layout:

<UserControl x:Class="SimpleDomEditorWpfSample.EventSequenceView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             ...>
	...
    <Grid>
        <ListView ...>
            <ListView.View>
                <GridView AllowsColumnReorder="False">
                    <GridViewColumn Header="Name">
					...
                    <GridViewColumn Header="Duration">
					...

Two columns, "Name" and "Duration", are defined.

Data Binding

Because EventSequenceView is a UserControl, it ultimately derives from FrameworkElement. FrameworkElement has the DataContext dependency property that defines the control's Source, which references the Binding's data source.

EventSequenceContext.OnNodeSet() sets DataContext for the EventSequenceView. Here, m_view holds the EventSequenceView instance:

m_view.DataContext = this;

The XAML also defines data binding for the ListView:

<ListView x:Name="m_listView" ItemsSource="{Binding Events}" 
		  SelectedItem="{Binding BindableSelection, Converter={StaticResource SelectionConverter}}">

We just saw that the Source for the EventSequenceView's ListView is EventSequenceContext. The XAML binding above sets the Path for the ListView's data binding to indicate which property on the source object to get and set the bound data value. It sets Path to the Events property of EventSequenceView. As a result, the EventSequenceView's ListView displays EventSequenceContext.Events.

EventSequenceContext defines a BindableSelection property:

/// <summary>
/// Exposes the Selection for two way data binding. Needed because the ISelectionContext.Selection
/// property is read-only.</summary>
public IEnumerable<object> BindableSelection
{
	get { return this.As<ISelectionContext>().Selection; }
	set { this.As<ISelectionContext>().SetRange(value); }
}

The second part of the XAML binding refers to this property:

SelectedItem="{Binding BindableSelection, Converter={StaticResource SelectionConverter}}">

Setting the binding this way means that when the user selects an event in the list, BindableSelection's set() method is called, and so we set the ISelectionContext's selection so all the appropriate ATF selection changed events are raised, and so the other views get updated. Thus the appropriate list shows up in the Resources window's ListView and the PropertyGrid in the Property window shows the properties of the selected event.

ISelectionContext.Selection itself is read-only, so the BindableSelection wrapper property was added to call SetRange() instead.

SelectionConverter is an IValueConverter that just handles the data type conversion between the displayed ListView.SelectedItem and the ISelectionContext.Selection. It is defined in SelectionConverter.cs. and has very basic implementations of Convert() and ConvertBack().

Finally, look at the bindings for the cell in the GridView cell that displays the data for a column:

<ListView.View>
	<GridView AllowsColumnReorder="False">
		<GridViewColumn Header="Name">
			<GridViewColumn.CellTemplate>
				<DataTemplate>
					<TextBlock Text="{Binding Name}" />

As previously seen, the EventSequenceView's ListView displays EventSequenceContext.Events — which is a list of Event objects. Each row in the ListView displays a single Event. And the binding above is to the Name property of the DOM adapter class Event, so the value of the Event's Name appears in the cell.

There is a similar binding for the Event.Duration property.

Behaviors

Behaviors are also defined for the ListView. Note that the namespace i is defined xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" at the beginning of the XAML file:

<ListView x:Name="m_listView" ItemsSource="{Binding Events}" 
		  SelectedItem="{Binding BindableSelection, Converter={StaticResource SelectionConverter}}">
	<i:Interaction.Behaviors>
		<behaviors:InstancingDropTargetBehavior/>
		<behaviors:ContextMenuBehavior/>
	</i:Interaction.Behaviors>

Interaction.Behaviors is an attached property holding a list of attached behaviors: InstancingDropTargetBehavior and ContextMenuBehavior, which are defined in ATF to handle drag and drop and context menu behaviors. For instance, InstancingDropTargetBehavior implements IDataObject to handle setting data after a drag and drop. InstancingDropTargetBehavior derives from the ATF class DropTargetBehavior<T>, which defines basic drag and drop functions and itself derives from Behavior<T>. SimpleDOMEditorWPF differs from SimpleDOMEditor by relying on WPF to handle drag and drop behavior, instead of implementing drag and drop directly.

ContextMenuBehavior Class Operation

This section follows through how ContextMenuBehavior operates for this sample.

After the sample application starts, OnAttached() is called. This method overrides the method in System.Windows.Interactivity.Behavior and is called when InitializeComponent() is called for the WPF controls:

protected override void OnAttached()
{
	base.OnAttached();
	AssociatedObject.MouseRightButtonUp += Element_MouseRightButtonUp;
}

AssociatedObject here is a System.Windows.Controls.ListView, so Element_MouseRightButtonUp() is called on MouseRightButtonUp events in the ListView.

OnAttached() is called multiple times: first for the ListView in the ResourceListView, and then for the ListView in the EventSequenceView control each time this control is created to display an event sequence document.

A user right-clicking in the ListView triggers the Element_MouseRightButtonUp() call:

private void Element_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
	object context = GetCommandContext(sender, e);
	object clickedData = GetCommandTarget(sender, e);
	
	if (context != null && clickedData != null)
	{
	...
	}
}

This method first gets a context for the event with GetCommandContext():

protected virtual object GetCommandContext(object sender, MouseButtonEventArgs e)
{
	var senderFwe = (FrameworkElement)sender;
	object context = Context;
	if (context == null)
		context = senderFwe.DataContext;
	return context;
}

The variable sender holds the ListView clicked on in the EventSequenceView control. Context is the DependencyProperty for ContextMenuBehavior and is null in this case. The returned context is the DataContext for EventSequenceView, which is EventSequenceContext, as noted in Data Binding.

Next, the event handler calls GetCommandTarget() to obtain the target item that was right-clicked:

protected virtual object GetCommandTarget(object sender, MouseButtonEventArgs e)
{
	// Default behavior to find command target is to just take the data context 
	// of the clicked element
	object clickedData = null;

	var originalSource = e.OriginalSource as DependencyObject;
	if (originalSource != null)
	{
		var fwe = originalSource.FindAncestor<FrameworkElement>();
		if (fwe != null)
			clickedData = fwe.DataContext;
	}

	return clickedData;
}

OriginalSource is a System.Windows.Controls.TextBlock here and this is also returned as the ancestor by FindAncestor<T>(). Its DataContext is SimpleDomEditorWpfSample.Event, the DOM adapter for event objects.

Because both context and clickedData are non-null, the next block of Element_MouseRightButtonUp() is executed:

if (context != null && clickedData != null)
{
	var service = Composer.Current.Container.GetExportedValueOrDefault<IContextMenuService>();
	if (service != null)
	{
		var providers = Composer.Current.Container.GetExportedValues<Atf.Applications.IContextMenuCommandProvider>();
		
		IEnumerable<object> commands =
		   Atf.Applications.ContextMenuCommandProvider.GetCommands(
			   providers,
			   context,
			   clickedData);

		service.RunContextMenu(commands, (FrameworkElement)sender, e.GetPosition((FrameworkElement)sender));

		e.Handled = true;
	}
}

GetExportedValueOrDefault<T>() sets service to an Sce.Atf.Wpf.Interop.ContextMenuService object. Then GetExportedValues<>() obtains a list of context menu providers: Sce.Atf.Wpf.Applications.StandardEditCommands and Sce.Atf.Wpf.Applications.HelpCommands.

Atf.Applications.ContextMenuCommandProvider.GetCommands() uses the list of providers to get an enumeration of commands for the context menu. Note that it takes the target's context and target item (clickedData) as parameters. In this context, HelpCommands doesn't apply, so only commands from StandardEditCommands are added to the enumeration.

Finally, the event handler uses the ContextMenuService to call RunContextMenu(), which displays a context menu listing menu items from the command enumeration.

ResourceListView Control

The ResourceListView control displays a list of resources associated with an event. ResourceListView is almost identical to EventSequenceView. The XAML files differ only in the names of the control and the column names.

Application Data Model

Type Definition

This application creates and edits event sequences. An event has attributes, such as duration, and can contain one or more resources, such as animations. The sample defines its data model in the XML Schema Definition (XSD) language in the type definition file eventSequence.xsd. This figure shows the Visual Studio XML Schema Explorer view of the sample's data definition:

This tree view shows that an "Event", eventType, contains a "Resource", resourceType, and has "name", "time", and "duration" attributes. The XML Schema for this type definition shows a different view of the same thing:

<!--Event, with name, start time, duration and a list of resources-->
<xs:complexType name ="eventType">
  <xs:sequence>
    <xs:element name="resource" type="resourceType" maxOccurs="unbounded"/>
  </xs:sequence>
  <xs:attribute name="name" type="xs:string"/>
  <xs:attribute name="time" type="xs:integer"/>
  <xs:attribute name="duration" type="xs:integer"/>
</xs:complexType>

The data model has a "Resource" type, and the types "Animation" and "Geometry" are based on "Resource". "Resource" types have the attributes "name", "size", and "compressed".

An event sequence is simply a sequence of any number of events, as this type definition shows:

<!--Event sequence, a sequence of events-->
<xs:complexType name ="eventSequenceType">
  <xs:sequence>
    <xs:element name="event" type="eventType" maxOccurs="unbounded"/>
  </xs:sequence>
</xs:complexType>

Note this definition for the root element:

<!--Declare the root element of the document-->
<xs:element name="eventSequence" type="eventSequenceType"/>

The root element is of "EventSequence" type, because a document contains one event sequence. This is very important, because this type has several DOM adapters defined for it, which can apply to the entire document.

Schema Class

The ATF DomGen utility generated a Schema class file from the type definition file. The file GenSchemaDef.bat contains commands for DomGen.

Schema contains subclasses for all the data types and fields for the attributes that are of the appropriate ATF DOM metadata classes.

For example, the "Resource" type has three AttributeInfo fields for its "name", "size", and "compressed" attributes:

public static class resourceType
{
    public static DomNodeType Type;
    public static AttributeInfo nameAttribute;
    public static AttributeInfo sizeAttribute;
    public static AttributeInfo compressedAttribute;
}

The "Event" type also has AttributeInfo fields for its attributes, plus a ChildInfo field for any resources contained in the object:

public static class eventType
{
    public static DomNodeType Type;
    public static AttributeInfo nameAttribute;
    public static AttributeInfo timeAttribute;
    public static AttributeInfo durationAttribute;
    public static ChildInfo resourceChild;
}

Even though an event may contain any number of resources, it needs only one ChildInfo object, because in the ATF DOM, a node can have either a single child or a single list of children in the DOM node tree.

Schema Loader Component

The SchemaLoader component loads the XML schema and does any other initialization required for the data model. It derives from XmlSchemaTypeLoader, which performs a substantial part of the schema loading process.

Its constructor, which is automatically called during MEF initialization when the component object is created, resolves the type definition file address and loads the file:

public SchemaLoader()
{
    // set resolver to locate embedded .xsd file
    SchemaResolver = new ResourceStreamResolver(Assembly.GetExecutingAssembly(), "SimpleDomEditorWpfSample/schemas");
    Load("eventSequence.xsd");
}

SchemaLoader constructors nearly always take this form. Other standard items are the NameSpace and TypeCollection properties:

public string NameSpace
{
    get { return m_namespace; }
}
private string m_namespace;
...
public XmlSchemaTypeCollection TypeCollection
{
    get { return m_typeCollection; }
}
private XmlSchemaTypeCollection m_typeCollection;

The other work to do is to define the method OnSchemaSetLoaded(), which is called after the schema set has been loaded and the DomNodeType objects have been created. This method accomplishes several things, described in the following sections.

Schema.Initialize

The schema loader calls Schema.Initialize() to initialize the Schema class. This method was also created by DomGen when it generated the rest of the metadata classes.

Define DOM Extensions

The loader defines DOM extensions for data types, so that methods in the various DOM adapters are called appropriately:

Schema.eventSequenceType.Type.Define(new ExtensionInfo<EventSequenceDocument>());
Schema.eventSequenceType.Type.Define(new ExtensionInfo<EventSequenceContext>());
Schema.eventSequenceType.Type.Define(new ExtensionInfo<MultipleHistoryContext>());
Schema.eventSequenceType.Type.Define(new ExtensionInfo<EventSequence>());
Schema.eventSequenceType.Type.Define(new ExtensionInfo<ReferenceValidator>());
Schema.eventSequenceType.Type.Define(new ExtensionInfo<UniqueIdValidator>());
Schema.eventSequenceType.Type.Define(new ExtensionInfo<DomNodeQueryable>());

Schema.eventType.Type.Define(new ExtensionInfo<Event>());
Schema.eventType.Type.Define(new ExtensionInfo<ResourceListContext>());

Schema.resourceType.Type.Define(new ExtensionInfo<Resource>());

Note that the types used here are the ones defined in the Schema class.

The first group's definitions all apply to Schema.eventSequenceType, which is the type of the document's root. These DOM adapters thus apply to the entire document and accomplish a number of purposes. A document describes a single sequence of events, so its DomNode tree has only one node of type "EventSequence", the tree root DomNode. This root node can be adapted to all of the adapters listed above. For instance, defining both EventSequenceDocument and EventSequenceContext as extensions of eventSequenceType makes it possible to convert between these types using As<>. For more details, see Working With Contexts.

The Schema.eventType and Schema.resourceType types also have DOM adapters. For more information on these adapters, see DOM Adapters.

Metadata Driven Property Editing

The schema loader enables metadata driven property editing for events and resources by creating an AdapterCreator:

var creator = new AdapterCreator<CustomTypeDescriptorNodeAdapter>();
Schema.eventType.Type.AddAdapterCreator(creator);
Schema.resourceType.Type.AddAdapterCreator(creator);

If this were not done, the property editors would not show properties from the property descriptors, defined later on.

Palette Type Setup

This step adds tag information to the type that is used later to add these types to the palette: "Event", "Animation", and "Geometry". The palette item information is encapsulated in a NodeTypePaletteItem object, a container class:

Schema.eventType.Type.SetTag(
    new NodeTypePaletteItem(
        Schema.eventType.Type,
        "Event".Localize(),
        "Event in a sequence".Localize(),
        "Events".Localize(),
        Resources.EventImage));

The NodeTypePaletteItem contains all the information needed to display and use the palette item: its type, name, descriptive text, category, and an icon image to appear on the palette and in the ListView controls.

For further details on how this sets up the palette, see Using a Palette.

Create Property Descriptors

Property descriptors for types specify the data that shows up in property editors for the types. For details on how this works, see Property Editing in ATF and Property Descriptors in particular.

The NamedMetadata.SetTag() method used below creates a PropertyDescriptorCollection containing an AttributePropertyDescriptor for each attribute of an "Event" type. This information is required for each attribute to appear in a property editor when an event is selected in the main ListView. The AdapterCreator must also be set up, as described previously in Metadata Driven Property Editing.

Schema.eventType.Type.SetTag(
    new PropertyDescriptorCollection(
        new PropertyDescriptor[] {
            new AttributePropertyDescriptor(
                "Name".Localize(),
                Schema.eventType.nameAttribute,
                null,
                "Event name".Localize(),
                false),
            new AttributePropertyDescriptor(
                "Time".Localize(),
                Schema.eventType.timeAttribute,
                null,
                "Event starting time".Localize(),
                false),
            new AttributePropertyDescriptor(
                "Duration".Localize(),
                Schema.eventType.durationAttribute,
                null,
                "Event duration".Localize(),
                false),
    }));

Using a Palette

SimpleDOMEditorWPF uses the WPF version of the ATF PaletteService component, which manages a palette of objects that can be dragged on to other controls. SimpleDOMEditorWPF allows dragging palette items onto the two ListView controls: the main ListView for events and the Resources ListView for resources of the selected event. PaletteService's constructor creates a PaletteContent view model for the palette and registers it with the ControlHostService component, placing the control on the left side of the main window.

Add Palette Items

SimpleDOMEditorWPF adds its own PaletteClient component that imports IPaletteService, which is satisfied by the PaletteService component. The IInitializable.Initialize() method for the PaletteClient component adds items to the palette:

void IInitializable.Initialize()
{
    NodeTypePaletteItem eventTag = Schema.eventType.Type.GetTag<NodeTypePaletteItem>();
    if (eventTag != null)
        m_paletteService.AddItem(eventTag, eventTag.Category, this);

    foreach (DomNodeType resourceType in m_schemaLoader.GetNodeTypes(Schema.resourceType.Type))
    {
        NodeTypePaletteItem resourceTag = resourceType.GetTag<NodeTypePaletteItem>();
        if (resourceTag != null)
            m_paletteService.AddItem(resourceTag, resourceTag.Category, this);
    }
}

This method adds items to the palette with the IPaletteService.AddItem() method, defined as:

void AddItem(object item, string categoryName, IPaletteClient client);

In the first call to AddItem(), the type for an event is added, Schema.eventType.Type.

The foreach loop iterates through all the types for the "Resource" type, Schema.resourceType.Type. The types "Animation" and "Geometry" are both based on the "Resource" type. For each of these "Resource" types, the loop checks that it can get a NodeTypePaletteItem for the type:

NodeTypePaletteItem resourceTag = resourceType.GetTag<NodeTypePaletteItem>();
if (resourceTag != null)
	m_paletteService.AddItem(resourceTag, resourceTag.Category, this);

Recall that in the SchemaLoader class, this kind of initialization occurs to add information for each "Resource" type for the palette:

string resourcesCategory = "Resources".Localize();
                
Schema.animationResourceType.Type.SetTag(
    new NodeTypePaletteItem(
        Schema.animationResourceType.Type,
        "Animation".Localize(),
        "Animation resource".Localize(),
        resourcesCategory,
        Resources.AnimationImage));

The call to GetTag() above simply retrieves the above NodeTypePaletteItem information, if present. After it verifies that the type has an associated NodeTypePaletteItem tag, Initialize() adds the type to the palette.

Looping in this way guarantees that all resource types are added to the palette and makes adding new resource types to the palette later easier.

Implement IPaletteClient

The IPaletteClient.GetInfo() method gets display information for the item. It retrieves the data from the NodeTypePaletteItem that was set in SchemaLoader:

void IPaletteClient.GetInfo(object item, ItemInfo info)
{
    NodeTypePaletteItem paletteItem = item.As<NodeTypePaletteItem>();
    if (paletteItem != null)
    {
        info.Label = paletteItem.Name;
        info.Description = paletteItem.Description;
    }
}

Convert() takes a palette item and returns an object that can be inserted into an IInstancingContext — a DomNode in this case:

object IPaletteClient.Convert(object item)
{
    DomNode node = null;
    NodeTypePaletteItem paletteItem = item.As<NodeTypePaletteItem>();
    if (paletteItem != null)
    {
        DomNodeType nodeType = paletteItem.NodeType;
        node = new DomNode(nodeType);

        if (nodeType.IdAttribute != null)
            node.SetAttribute(nodeType.IdAttribute, paletteItem.Name); // unique id, for referencing

        if (nodeType == Schema.eventType.Type)
            node.SetAttribute(Schema.eventType.nameAttribute, paletteItem.Name);
        else if (Schema.resourceType.Type.IsAssignableFrom(nodeType))
            node.SetAttribute(Schema.resourceType.nameAttribute, paletteItem.Name);
    }
    return node;
}

The item parameter in Convert() is a selected item in the palette. This method first verifies that it can adapt the given item to a NodeTypePaletteItem. If so, it obtains the DomNodeType of the type associated with the palette item and creates a new DomNode of that DomNodeType.

Next, the IdAttribute of the DomNode is set, if it wasn't already, with the DomNode.SetAttribute() method:

public void SetAttribute(AttributeInfo attributeInfo, object value);

Finally, it sets the name attribute of the DomNode. The type of item is checked, so that the proper nameAttribute is set.

DOM Adapters

DOM adapters allow a DomNode to be dynamically cast to another interface. SimpleDOMEditorWPF provides the DOM adapters Event, EventSequence, and Resource for their corresponding types. All these adapters do is provide properties that access information in a DomNode. For instance, Event has a Name property:

/// Gets or sets name associated with event, such as a label
public string Name
{
    get { return GetAttribute<string>(Schema.eventType.nameAttribute); }
    set { SetAttribute(Schema.eventType.nameAttribute, value); }
}

The DomNodeAdapter.GetAttribute() method gets the value of a specified attribute, Schema.eventType.nameAttribute, defined in the Schema class as

public static AttributeInfo nameAttribute;

Most of the DOM adapter properties simply access attribute values of the type, but the EventSequence DOM adapter's Events property gets all the events in an event sequence:

/// Gets list of Events in sequence
public IList<Event> Events
{
    get { return GetChildList<Event>(Schema.eventSequenceType.eventChild); }
}

Note that the returned property value is an IList<Event>, that is, a list of the DOM adapter Event objects associated with an event sequence DOM adapter EventSequence.

The DOM adapters in this application provide a simple way to get properties for the event, event sequence, and resource objects, all embodied in DomNode objects.

Working With Contexts

SimpleDOMEditorWPF relies heavily on contexts. A context provides services for operations in certain situations, hence the name context. ATF provides quite a few interfaces and classes for different types of contexts, and this sample uses several. For information about contexts in general, see ATF Contexts.

This section illustrates how contexts control and facilitate editing operations in the main and Resources ListView controls.

ResourceListContext Class

ResourceListContext provides the editing context for the ResourceListView in the Resources window, manages DOM events, and handles resources belonging to the selected event. This class adapts the application data to a list and uses contexts to enable data editing as well as undoing and redoing data changes. In this respect, it is similar to the EventContext class in the SimpleDOMEditor sample.

ResourceListContext has this derivation ancestry: EditingContext, HistoryContext, TransactionContext, and finally DomNodeAdapter, so all these context classes are DOM adapters. EditingContext is a history context with a selection, providing a basic self-contained editing context. Several samples, such as ATF Fsm Editor Sample and ATF State Chart Editor Sample have context classes that extend EditingContext. For more information about this context, see General Purpose EditingContext Class.

As its declaration shows, ResourceListContext also implements several interfaces:

public class ResourceListContext : EditingContext,
    IObservableContext,
    IInstancingContext,
    IEnumerableContext,
    INotifyPropertyChanged

The ancestor classes of EditingContext also implement some useful interfaces, such as IHistoryContext and ITransactionContext, which allow changes in the context, such as deleting resources in an event, to be undone and redone.

ResourceListContext's constructor creates a new ResourceListView object, in which resources are listed for the selected event.

ResourceListContext Properties

Several useful properties are defined:

  • View: Get and set the ResourceListView object.
  • Items: Get a list of the event's Resource objects.
  • Resources: Get IEnumerable<Resource> for the event's resources.
  • BindableSelection: Gets and sets the selection. It exposes the selection for two way data binding and is needed because the ISelectionContext.Selection property is read-only. This property is bound to the SelectedItem in ResourceListView.xaml.

ResourceListContext OnNodeSet Method

As a DOM adapter, ResourceListContext's OnNodeSet() method subscribes to several events for a DomNode, such as ChildInserted:

protected override void OnNodeSet()
{
    ...
    DomNode.ChildInserted += DomNode_ChildInserted;
    ...
}
...
private void DomNode_ChildInserted(object sender, ChildEventArgs e)
{
    Resource resource = e.Child.As<Resource>();
    if (resource != null)
    {
        ItemInserted.Raise(this, new ItemInsertedEventArgs<object>(e.Index, resource));
    }
	
    OnPropertyChanged();
}
...
private void OnPropertyChanged()
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs("Resources"));
    }
}

If this handler can adapt the child DomNode e.Child to the Resource DOM adapter, it raises the ItemInserted event, which is defined in the IObservableContext interface.

The other event handlers defined deal with Resource objects, too, in a similar way.

ResourceListContext INotifyPropertyChanged Interface

This WPF interface notifies clients that a property value has changed for the WPF property system. It contains the PropertyChanged event, which should be raised any time a property changes. Numerous functions in ResourceListContext raise this event.

ResourceListContext IInstancingContext Interface

IInstancingContext handles instancing in the Resources ListView, that is, working with resource instances in this control, which can be edited using menu items and by drag and drop. You insert resources in the Resources ListView for an event by dragging and dropping a resource from the palette onto the Resources ListView. Drag and drop behavior is governed by the InstancingDropTargetBehavior WPF behavior class.

The IInstancingContext interface provides methods to check whether items can be copied, inserted (pasted), or deleted, and performs these actions, too.

Here are the copy related methods:

public bool CanCopy()
{
    return Selection.Count > 0;
}
...
public object Copy()
{
    IEnumerable<DomNode> resources = Selection.AsIEnumerable<DomNode>();
    List<object> copies = new List<object>(DomNode.Copy(resources));
    return new DataObject(copies.ToArray());
}

Selection is defined in the EditingContext class and is an AdaptableSelection representing a collection of selected objects; this class handles basic selection mechanics. CanCopy() uses Selection to simply verify that there is at least one selected item. Copy() adapts the selection to a collection of DomNodes, copies them as a list of objects, and then constructs a System.Windows.Forms.DataObject from the copy, which can be placed on the Windows® clipboard. The StandardEditCommands component actually calls Copy() and puts the copied data onto the clipboard.

CanInsert() and Insert() do the following:

public bool CanInsert(object insertingObject)
{
    IDataObject dataObject = (IDataObject)insertingObject;
    object[] items = dataObject.GetData(typeof(object[])) as object[];
    if (items == null)
        return false;

    foreach (object item in items)
        if (!Adapters.Is<Resource>(item))
            return false;

    return true;
}
...
public void Insert(object insertingObject)
{
    IDataObject dataObject = (IDataObject)insertingObject;
    object[] items = dataObject.GetData(typeof(object[])) as object[];
    if (items == null)
        return;

    DomNode[] itemCopies = DomNode.Copy(Adapters.AsIEnumerable<DomNode>(items));
    IList<Resource> resources = this.Cast<Event>().Resources;
    foreach (Resource resource in Adapters.AsIEnumerable<Resource>(itemCopies))
        resources.Add(resource);

    Selection.SetRange(itemCopies);
}

The first part of both methods is identical, simply verifying that there are items to insert.

StandardEditCommands calls CanInsert() to determine whether the Edit > Insert menu item is enabled or not. CanInsert() casts the inserted data as a IDataObject and then casts it to an array of selected objects, each of which is a DomNode of type "Resource". The second part of CanInsert() checks whether all the selected items can be adapted as Resource objects. If so, the method returns true, and false otherwise.

For the insert operation, StandardEditCommands gets the clipboard data and calls Insert() to insert this data. Insert() casts the inserted data as a IDataObject and then casts it to an array of selected objects, just as for CanInsert(). These objects are adapted to DomNodes and then copied.

In this method, this represents a ResourceListContext object, which is an adapted DomNode of type "Event" representing a selected event in the main ListView. Both ResourceListContext and Event are DOM adapters for the "Event" type, as shown in these lines from SchemaLoader where DOM adapters are defined:

Schema.eventType.Type.Define(new ExtensionInfo<Event>());
Schema.eventType.Type.Define(new ExtensionInfo<ResourceListContext>());

Because both of the DOM adapters are defined for the "Event" type, DomNodes of these types can be adapted to each other. So this DomNode can be adapted to an Event object, and its list of resources can be obtained from the Resources property of the Event DOM adapter:

IList<Resource> resources = this.Cast<Event>().Resources;

Finally, each inserted DomNode is adapted to a Resource object and added to the list of resources for the selected Event in the main ListView.

The deletion operations are simpler:

public bool CanDelete()
{
    return Selection.Count > 0;
}
...
public void Delete()
{
    List<DomNode> nodesToRemove = new List<DomNode>();
    foreach (DomNode node in Selection.AsIEnumerable<DomNode>())
        nodesToRemove.Add(node);

    foreach (DomNode node in nodesToRemove)
        node.RemoveFromParent();

    Selection.Clear();
    OnPropertyChanged();
}

CanDelete() is identical to CanCopy(), simply verifying that there's something selected to delete.

Delete() iterates all selected items' underlying DomNodes and adds them to a temporary list. It then removes each one from the parent with DomNode.RemoveFromParent(), effectively removing them from the application data. It also clears the selection using the Selection.Clear() method. Finally it calls OnPropertyChanged() to raise the PropertyChanged event so displays are updated.

EventSequenceContext Class

EventSequenceContext handles the logic of document related events and passes them on to the EventSequenceView, which lists a sequence of events.

EventSequenceContext has many similarities to ResourceListContext, including its derivation ancestry from EditingContext. One of the differences is that EventSequenceContext's constructor creates an EventSequenceView instance used to display events, while ResourceListContext uses the ResourceListView defined in ResourceListEditor. This is because there can be multiple EventSequenceDocuments open, each requiring its own EventSequenceView, but there is only ever one ResourceListEditor open, so it can maintain a single ResourceListView.

EventSequenceContext implements all the interfaces that ResourceListContext does. Their implementations are very similar, but Resource objects are replaced by Event objects in EventSequenceContext. The IInstancingContext implementations look identical except for that substitution, for example.

One other important difference from ResourceListContext is that EventSequenceContext's OnNodeSet() method does this:

m_view.DataContext = this;

The field m_view contains the EventSequenceView. Setting its DataContext dependency property to this, i.e., the EventSequenceContext, means that the Source dependency property for the EventSequenceView is the EventSequenceContext object.

EventSequenceContext Properties

Several useful properties are defined:

  • ControlInfo: Gets and sets basic info about the hosted control such as its name and dock location.
  • Document: Gets and sets the DOM document being displayed.
  • View: Get the EventSequenceView object.
  • Items: Get a list of the Event objects.
  • Events: Get IEnumerable<Event> for the events.
  • BindableSelection: Gets and sets the selection. It exposes the selection for two way data binding and is needed because the ISelectionContext.Selection property is read-only. This property is bound to the SelectedItem in EventSequenceView.xaml.

Document Handling

Document handling applications typically implement both IDocument for a document object and IDocumentClient for the document client.

EventSequenceDocument Class

EventSequenceDocument represents document data and extends DomDocument, which implements IDocument.

The DomDocument class provides the basics of a document: IsReadOnly and Dirty properties, a DirtyChanged event, and the OnDirtyChanged() and OnReloaded event handlers. DomDocument extends DomResource and implements IResource to provide the resource properties Type and Uri to describe a document's type and location.

EventSequenceDocument itself implements overrides for the property Type and the methods OnUriChanged() and OnDirtyChanged() for application-specific behavior. These latter event handlers simply execute the base methods, as well as update ControlInfo for the ListView displaying the new document. ControlInfo holds information about controls hosted by ControlHostService.

Editor Component Document Client

The Editor component tracks documents with the DocumentRegistry, registers the editor view control EventSequenceView with the ControlHostService, and tracks the active context for the ContextRegistry. Editor is the client for event sequence documents.

DocumentClientInfo Class

Editor uses the DocumentClientInfo class to hold document editor information. The Info property gets the DocumentClientInfo from a static variable:

public DocumentClientInfo Info
{
    get { return DocumentClientInfo; }
}

/// <summary>
/// Information about the document client</summary>
public static DocumentClientInfo DocumentClientInfo = new DocumentClientInfo(
    "Event Sequence".Localize(),
    new string[] { ".xml", ".esq" },
    Sce.Atf.Resources.DocumentImage,
    Sce.Atf.Resources.FolderImage,
    true);

Opening a Document

CanOpen() simply checks that the filename extension is suitable, using the DocumentClientInfo:

public bool CanOpen(Uri uri)
{
    return DocumentClientInfo.IsCompatibleUri(uri);
}

The Open() method reads the document file's data and converts it into a tree of DomNode objects. After that, it sets up the context and other information needed for the new document. The initial phase of opening creates a DomNode for the tree root and gets the file name:

public IDocument Open(Uri uri)
{
    DomNode node = null;
    string filePath = uri.LocalPath;
    string fileName = Path.GetFileName(filePath);

For an existing document, a FileStream is created and read. Because the document is stored as an XML document, you can use an DomXmlReader to read data and convert it to a tree of DomNodes. DomXmlReader.Read() returns the root DomNode of the tree it creates. For a new document, a DomNode with the DomNodeType of the event sequence root element — from the Schema class — is created.

if (File.Exists(filePath))
{
    // read existing document using standard XML reader
    using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
    {
        DomXmlReader reader = new DomXmlReader(m_schemaLoader);
        node = reader.Read(stream, uri);
    }
}
else
{
    // create new document by creating a Dom node of the root type defined by the schema
    node = new DomNode(Schema.eventSequenceType.Type, Schema.eventSequenceRootElement);
}

Next, Open() performs a series of ATF DOM set up operations to complete the open process:

EventSequenceDocument document = null;
if (node != null)
{
    // Initialize Dom extensions now that the data is complete
    node.InitializeExtensions();

    EventSequenceContext context = node.As<EventSequenceContext>();

    ControlInfo controlInfo = new ControlInfo(Path.Combine(filePath, fileName),
        StandardControlGroup.Center,
        new DockContent(null, null), this);
    context.ControlInfo = controlInfo;

    // set document URI
    document = node.As<EventSequenceDocument>();
    document.Uri = uri;

    context.Document = document;

    // show the document editor
    // This line requires references to System.Drawing and System.Windows.Forms. Would really like to remove those dependencies!
    m_controlHostService.RegisterControl(context.View, 
        fileName, 
        "Event sequence document", 
        StandardControlGroup.Center, 
        Path.Combine(filePath, fileName), 
        this);
}

return document;

First, a null EventSequenceDocument object is created.

InitializeExtensions() initializes all the DOM adapters defined in the schema loader, shown in Define DOM Extensions.

An EventSequenceContext is created by adapting the root DomNode to EventSequenceContext, which is a DOM adapter for the "EventSequence" type. Recall that the "EventSequence" type is the type of the document's root.

The event sequence document's data is going to be displayed in a EventSequenceView. A ControlInfo is set up for this control and is placed in the EventSequenceContext ControlInfo property.

EventSequenceDocument is also a DOM adapter for the "EventSequence" type, so the tree root DomNode can be adapted to EventSequenceDocument.

The document is saved in the context's Document property for future reference:

context.Document = document;

Finally, the EventSequenceView control is registered with the ControlHostService component.

Saving a Document

Save() uses the capabilities of DomXmlWriter to write the DomNode tree to an XML file. DomXmlWriter gets data model information from the schema so it knows how to write the DomNode tree to XML with its Write() method. The document is cast back to an EventSequenceDocument to get its root DomNode for Write().

public void Save(IDocument document, Uri uri)
{
    string filePath = uri.LocalPath;
    FileMode fileMode = File.Exists(filePath) ? FileMode.Truncate : FileMode.OpenOrCreate;
    using (FileStream stream = new FileStream(filePath, fileMode))
    {
        DomXmlWriter writer = new DomXmlWriter(m_schemaLoader.TypeCollection);
        EventSequenceDocument eventSequenceDocument = (EventSequenceDocument)document;
        writer.Write(eventSequenceDocument.DomNode, stream, uri);
    }
}

Closing a Document

Closing the document entails cleaning up the contexts that were used:

public void Close(IDocument document)
{
    EventSequenceContext context = Adapters.As<EventSequenceContext>(document);
    m_controlHostService.UnregisterContent(context.View);
    context.ControlInfo = null;

    // close all active EditingContexts in the document
    foreach (DomNode node in context.DomNode.Subtree)
        foreach (EditingContext editingContext in node.AsAll<EditingContext>())
            m_contextRegistry.RemoveContext(editingContext);

    // close the document
    m_documentRegistry.Remove(document);
}

The EventSequenceView control is unregistered.

Finally, any EditingContexts that were created are removed from the ContextRegistry, and the document is removed from the DocumentRegistry.

Contexts are discussed in Working With Contexts.

Editor Component Control Host Client

Editor also implements IControlHostClient for the EventSequenceView. This requires it to handle the control's activation, deactivation, and close events.

The Activate() method updates the document and context registries:

void IControlHostClient.Activate(Control control)
{
    var view = control as EventSequenceView;
    if (view != null)
    {
        var context = view.DataContext as EventSequenceContext;
        if (context != null)
        {
            EventSequenceDocument document = context.Document;
            if (document != null)
            {
                m_documentRegistry.ActiveDocument = document;
                m_contextRegistry.ActiveContext = context;
            }
        }
    }
}

It casts the control to EventSequenceView, which is a Control. Assuming this cast succeeds, it retrieves the EventSequenceContext from the DataContext property of the EventSequenceView in which it was stored. Finally, it obtains the EventSequenceDocument from the EventSequenceContext's Document property and sets it as the active document in the DocumentRegistry component. It sets the EventSequenceContext as the active context with the ContextRegistry.

Close() performs a similar operation to Activate(), but with the aim of closing things out. It uses the IDocumentService in the variable m_documentService, provided by the StandardFileCommands component in this sample, to close the document. If it succeeds, it then removes the context associated with the document from the ContextRegistry.

bool IControlHostClient.Close(Control control)
{
    bool closed = true;

    var view = control as EventSequenceView;
    if (view != null)
    {
        var context = view.DataContext as EventSequenceContext;
        if (context != null)
        {
            EventSequenceDocument document = context.Document;
            if (document != null)
            {
                closed = m_documentService.Close(document);
                if (closed)
                    m_contextRegistry.RemoveContext(document);
            }
        }
    }
    return closed;
}

Close() obtains the EventSequenceDocument the same way that Activate() does.

ResourceListEditor Component

The ResourceListEditor component creates the ResourceListView and registers it with the ControlHostService, and handles IControlHostClient events and context changed events. It handles displaying resources in the ResourceListView. Resources are edited by either dragging and dropping them from the palette onto a ListView or deleting them from the ResourceListView. You can also edit resources with context menus in the ResourceListView. Resource attributes can also be edited, but that falls under property editing, which is all performed by the PropertyEditor component.

Drag and drop behavior is governed by the InstancingDropTargetBehavior WPF behavior class, and context menus by the ContextMenuBehavior WPF behavior class. These functions are not handled in ResourceListEditor.

ResourceListEditor Constructor

Because the ResourceListEditor shows the resources for the currently selected event, this component must track context changes. The ResourceListEditor constructor sets up event handling for context changes, that is, tracking which ResourceListContext is active:

m_contextRegistry.ActiveContextChanged += new EventHandler(contextRegistry_ActiveContextChanged);

The ActiveContextChanged event handler, contextRegistry_ActiveContextChanged, gets the current EventSequenceContext and, if it is different from the previous EventSequenceContext and non-null, unsubscribes the previous context from the SelectionChanged event and subscribes the new context to the event:

private void contextRegistry_ActiveContextChanged(object sender, EventArgs e)
{
    // make sure we're always tracking the most recently active EventSequenceContext
    EventSequenceContext context = m_contextRegistry.GetMostRecentContext<EventSequenceContext>();
    if (m_eventSequenceContext != context)
    {
        if (m_eventSequenceContext != null)
        {
            m_eventSequenceContext.SelectionChanged -= new EventHandler(eventSequenceContext_SelectionChanged);
        }

        m_eventSequenceContext = context;

        if (m_eventSequenceContext != null)
        {
            // track the most recently active EventSequenceContext's selection to get the most recently
            //  selected event.
            m_eventSequenceContext.SelectionChanged += new EventHandler(eventSequenceContext_SelectionChanged);
        }

        UpdateEvent();
    }
}

The UpdateEvent() call updates the ResourceListContext in the Context Registry. It also updates the ResourceListView's DataContext property (so the data source is updated) and sets the eventContext's View property to the registered m_resourceListView:

private void UpdateEvent()
{
    Event nextEvent = null;
    if (m_eventSequenceContext != null)
        nextEvent = m_eventSequenceContext.Selection.GetLastSelected<Event>();

    if (m_event != nextEvent)
    {
        // remove last event's editing context in case it was activated
        if (m_event != null)
            m_contextRegistry.RemoveContext(m_event.Cast<ResourceListContext>());

        m_event = nextEvent;

        // get next event's editing context and bind to resources list view
        ResourceListContext eventContext = null;
        if (nextEvent != null)
        {
            eventContext = nextEvent.Cast<ResourceListContext>();
            eventContext.View = m_resourceListView;
        }

        m_resourceListView.DataContext = eventContext;
    }
}

In this method, Event refers to event data that is edited by this sample, not a subscribable event.

Because application data is in an ATF DOM, an event is represented by a DomNode. Such a DomNode can be adapted to both Event and ResourceListContext DOM node adapters, as indicated by this definition in the SchemaLoader for the "Event" type:

Schema.eventType.Type.Define(new ExtensionInfo<Event>());
Schema.eventType.Type.Define(new ExtensionInfo<ResourceListContext>());

Both these adaptations are used in the UpdateEvent() method. This method gets the last selected event item in the current EventSequenceContext's ListView as an Event in this line:

nextEvent = m_eventSequenceContext.Selection.GetLastSelected<Event>();

If nextEvent is different from the previous Event, m_event, then m_event is adapted to ResourceListContext and removed from the ContextRegistry:

m_contextRegistry.RemoveContext(m_event.Cast<ResourceListContext>());

Finally, the last selected event, nextEvent, is adapted to ResourceListContext and used to update the ResourceListEditor's ListView:

if (nextEvent != null)
{
    eventContext = nextEvent.Cast<ResourceListContext>();
    eventContext.View = m_resourceListView;
}

m_resourceListView.DataContext = eventContext;

The last line sets the DataContext property in the ResourceListView. This makes the ResourceListContext object the Source dependency property for the ResourceListView.

ResourceListEditor IInitializable.Initialize

The IInitializable.Initialize() function creates a new ResourceListView and registers this control.

void IInitializable.Initialize()
{
    m_resourceListView = new ResourceListView();

    m_resourcesControlDef = new ControlDef()
    {
        Name = "Resources".Localize(),
        Description = "Resources for selected Event".Localize(),
        Group = StandardControlGroup.Bottom,
        Id = s_resourceListEditorId.ToString()
    };

    m_controlHostService.RegisterControl(m_resourcesControlDef, m_resourceListView, this);
}

ResourceListEditor IControlHostClient Interface

This interface mainly updates the active ResourceListContext when the ResourceListView is activated in Activate().

void IControlHostClient.Activate(object control)
{
    // if our ListView has become active, make the last selected event
    //  the current context.
    if (control == m_resourceListView)
    {
        if (m_event != null)
            m_contextRegistry.ActiveContext = m_event.Cast<ResourceListContext>();
    }
}

Topics in this section

Clone this wiki locally