Event after SelectionChanged?

Hello,

In my Diagram I have some nodes marking rooms, called RoomNodeData. There’s a ClickCreatingTool, which creates those nodes upon double click. These room nodes contain a property RoomSize, which is supposed to be measured in the diagram by drawing a polygon and upon closing the polygon, the size is calculated and set in the room node.

Therefore I have a button, which changes the diagram’s model and (de)activates or changes certain tools upon clicked. Now every node other than room nodes gets deactivated and the user can select one of the existing room nodes. After doing so, a new ClickCreatingTool gets activated, with which the user can set polygon nodes via single click. Upon clicking the first drawn polygon node, the ClickCreatingTool gets deactivated and the currently selected room deselected, so the user can select another room to measure. He can also deactivate the tool and if he does, the Diagram’s previous state gets restored so the user can insert room nodes again.

You can see the whole process in the following pictures.

1. room node created via double click

2. measure room size selected after more rooms have been created and linked
All room nodes are now selectable, no more room nodes can be created via double click, no other node or link can be selected.

private void OnToggleStateChanged(BarCheckItem barCheckItem) {
    switch (barCheckItem.BarItemName) {
        case "MeasureRoomLayout":
            if (barCheckItem.IsChecked.HasValue) {
                // Set cursor
                ...

                if (barCheckItem.IsChecked.Value) {
                    // Switch layers for newly created nodes
                    DiagramHandler.SetActiveLayers(base.Diagram.Panel.Children.OfType<ILayer>().Where(layer => layer.NavigationStep == NavigationStep.Rooms && layer.IsForeground && layer.IsBlendLayer));

                    // Disable drag selecting tool
                    base.Diagram.DragSelectingTool.MouseEnabled = false;
                   
                    // Set new tools
                    base.Diagram.ClickSelectingTool = new MeasureRoomLayoutSelectingTool(base.Diagram);
                    base.Diagram.ClickCreatingTool = new MeasureRoomLayoutCreatingTool() { MouseEnabled = false };
                    base.Diagram.DraggingTool = new MeasureRoomDraggingTool(CalculateRoomSize);

                    // Set new event handlers
                    base.Diagram.SelectionChanged += OnSelectionChanged;
                    base.Diagram.NodeCreated += OnPolygonPointNodeCreated;

                    // This part is not relevant yet (see later)
                    if (this.ReopenSettingsNode != null) {
                        base.Diagram.Select(reopenSettingsNode);
                    }
                } else {
                    // Do things upon deactivating measuring tool (see 6.)
                    ...
                }
            }
            break;
    }
}

3. room node selected
All other nodes are now unselectable in addition to all previously disabled nodes. Now the user can start drawing the polygon.

private new void OnSelectionChanged(object sender, SelectionChangedEventArgs e) {
    Northwoods.GoXam.Diagram diagram = (Northwoods.GoXam.Diagram)sender;
    IEnumerable<AstaNodeData> selectedNodeData = diagram.SelectedParts.Select(part => part.Data).OfType<AstaNodeData>().ToList();

    if (diagram.ClickSelectingTool is MeasureRoomLayoutSelectingTool) {
        // Check if room was selected and set selected room
        if (selectedNodeData.OfType<RoomNodeData>().Any()) {
            ((MeasureRoomLayoutSelectingTool)diagram.ClickSelectingTool).SelectedRoom = selectedNodeData.OfType<RoomNodeData>().First();
            // Activate click creating tool for polygon nodes
            diagram.ClickCreatingTool.MouseEnabled = true;
        }
        // Calculate area (see 5.)
        else {
            ...
        }
    }
}

4. creating polygon nodes via single click
Polygon nodes are automatically linked and only drawn when clicked on free space, hence created nodes can be moved afterwards.

public void OnPolygonPointNodeCreated(object sender, DiagramEventArgs e) {
    Northwoods.GoXam.Diagram diagram = (Northwoods.GoXam.Diagram)sender;

    // Get current and last polygon nodes
    PolygonPointNodeData currentPolygonPoint = diagram.SelectedParts.Select(part => part.Data).OfType<PolygonPointNodeData>().First();
    PolygonPointNodeData previousPolygonPoint = diagram.NodesSource.OfType<PolygonPointNodeData>().Where(polygonPoint => polygonPoint.AssociatedRoom == currentPolygonPoint.AssociatedRoom).OrderByDescending(polygonPoint => polygonPoint.CreationTime).FirstOrDefault(polygonPoint => polygonPoint != currentPolygonPoint);

    // Draw a link between the polygon points
    if (previousPolygonPoint != null) {
        ((ObservableCollection<AstaLinkData>)diagram.LinksSource).Add(new PolygonPointLinkData() { From = currentPolygonPoint.Key, FromId = currentPolygonPoint.Guid, To = previousPolygonPoint.Key, ToId = previousPolygonPoint.Guid, AssociatedRoom = currentPolygonPoint.AssociatedRoom });
    }
}

5. Click the first polygon node to close the polygon
Polygon nodes and links marked grey but are still moveable to enable room size changes after drawing. All room nodes become selectable again so the user can measure other rooms.

private new void OnSelectionChanged(object sender, SelectionChangedEventArgs e) {
    Northwoods.GoXam.Diagram diagram = (Northwoods.GoXam.Diagram)sender;
    IEnumerable<AstaNodeData> selectedNodeData = diagram.SelectedParts.Select(part => part.Data).OfType<AstaNodeData>().ToList();

    if (diagram.ClickSelectingTool is MeasureRoomLayoutSelectingTool) {
        // Check if room was clicked and set selected room (see 3.)
        if (selectedNodeData.OfType<RoomNodeData>().Any()) {
            ...
        } else {
            RoomNodeData selectedRoom = ((MeasureRoomLayoutSelectingTool)diagram.ClickSelectingTool).SelectedRoom;
            // Check if first polygon node was clicked on, thus close polygon
            if (diagram.NodesSource.OfType<PolygonPointNodeData>().Count(polygonPoint => polygonPoint.AssociatedRoom == selectedRoom) > 1 && diagram.SelectedParts.Select(node => node.Data).OfType<PolygonPointNodeData>().Any(selectedPolygonPoint => diagram.NodesSource.OfType<PolygonPointNodeData>().Where(polygonPoint => polygonPoint.AssociatedRoom == selectedRoom).MinBy(polygonPoint => polygonPoint.CreationTime) == selectedPolygonPoint)) {
                // Call link drawing function without creating new node
                OnPolygonPointNodeCreated(sender, new DiagramEventArgs());
                // Calculate area
                ((MeasureRoomLayoutSelectingTool)diagram.ClickSelectingTool).SelectedRoom.Size.SquareMeter = CalculateRoomSize(base.Diagram.NodesSource.OfType<PolygonPointNodeData>().Where(polygonPoint => polygonPoint.AssociatedRoom == ((MeasureRoomLayoutSelectingTool)diagram.ClickSelectingTool).SelectedRoom).OrderBy(polygonPoint => polygonPoint.CreationTime).Select(polygonPoint => polygonPoint.Location).ToList());
                ((MeasureRoomLayoutSelectingTool)diagram.ClickSelectingTool).SelectedRoom = null;
                // Disable polygon node creating tool
                diagram.ClickCreatingTool.MouseEnabled = false;

                // This part is not relevant yet (see later)
                if (this.ReopenSettingsNode != null) {
                    this.ReopenSettingsNode = null;
                    EventAggregator.GetEvent<BarCheckItemResetStateEvent>().Publish(string.Empty);
                }
            }
    }
}

6. Deactivate measure room size tool
When the user has measured the rooms, he can disable the tool by clicking the button again, thus he’s able to create other rooms afterwards.

private void OnToggleStateChanged(BarCheckItem barCheckItem) {
    switch (barCheckItem.BarItemName) {
        case "MeasureRoomLayout":
            if (barCheckItem.IsChecked.HasValue) {
                // Set cursor
                ...

                if (barCheckItem.IsChecked.Value) {
                    // Do things upon activating measuring tool (see 2.)
                } else {
                    // Switch back layers for room nodes
                    DiagramHandler.SetActiveLayers(base.Diagram.GetAstaLayers(layer => layer.Key == new Guid((string)this.NavigationContext.Parameters[NavigationParameter.Key]) && layer.NavigationStep == NavigationStep.Rooms && layer.Floor == DiagramHandler.ActiveNodeLayer.Floor && layer.IsForeground && !layer.IsBlendLayer));

                    base.Diagram.DragSelectingTool.MouseEnabled = true;

                    // Set tools to the regular ones
                    base.Diagram.ClickSelectingTool = new ClickSelectingTool();
                    base.Diagram.ClickCreatingTool = new AstaClickCreatingTool() { PrototypeData = new RoomNodeData(base.Diagram.NodesSource.OfType<RoomNodeData>().Count().ToString()) };
                    base.Diagram.DraggingTool = new DraggingTool();

                    // Remove event handlers
                    base.Diagram.SelectionChanged -= OnSelectionChanged;
                    base.Diagram.NodeCreated -= OnPolygonPointNodeCreated;

                    // Remove polygon points and links
                    base.Diagram.NodesSource.OfType<PolygonPointNodeData>().ToList().ForEach(polygonPoint => ((ObservableCollection<AstaNodeData>)base.Diagram.NodesSource).Remove(polygonPoint));
                    base.Diagram.LinksSource.OfType<PolygonPointLinkData>().ToList().ForEach(polygonLink => ((ObservableCollection<AstaLinkData>)base.Diagram.LinksSource).Remove(polygonLink));

                    // Reactivate all other nodes
                    base.Diagram.NodesSource.OfType<ApartmentNodeData>().ForEach(apartment => apartment.Status = PartStatus.Default);
                    base.Diagram.LinksSource.OfType<RoomLinkData>().ForEach(link => link.Status = PartStatus.Default);
                }
            }
            break;
    }
}

Now this is working very well so far. However, there’s another way of measuring. If the user double clicks a room node, a popup opens with various options for the user to insert data. In that popup is another measure room size button with which the user can only measure the size for this very room. In this case the popup data gets cached, the popup gets closed, the measure room size button automatically selected, which throws its event, thus room selection becomes active and the room, whose settings were opened gets automatically selected. Now the user can draw his polygon and upon finishing the button gets automatically deactivated and the popup opens up again and fills itself with the cached data, so the user can finish the form.

Here’s an image of it and the following happening:

public void OnSettingsRequest(RoomNodeData nodeData, MouseButtonEventArgs eventArgs)
{
    RoomSettingsRequest.Raise(new RoomSettingsNotification(/* Pass various values */) { Title = $"{Resources.RoomViewModel_PropertiesForRoom} {nodeData.Key}" }, response => {
        if (response == null) { return; }

        if (response.Confirmed) {
            // Apply data
            ...
        } else if (response.Delayed) {
            // Trigger the button click
            EventAggregator.GetEvent<BarCheckItemSetStateEvent>().Publish("MeasureRoomLayout");
            // Remember the currently selected room node
            this.ReopenSettingsNode = nodeData;
        }
    });
}

Now, there were two parts in the code above I marked as irrelevant yet. These become relevant now.
There’s this one when the measure room size button’s event gets handled, which automatically selects the proper room node:

private void OnToggleStateChanged(BarCheckItem barCheckItem) {
    ...
                if (this.ReopenSettingsNode != null) {
                    base.Diagram.Select(reopenSettingsNode);
                }
    ...
}

And there’s this one when the polygon gets closed, calling the measure room size’s event to disable room measuring:

private new void OnSelectionChanged(object sender, SelectionChangedEventArgs e) {
    ...
                if (this.ReopenSettingsNode != null) {
                    this.ReopenSettingsNode = null;
                    // Trigger the button click once again to deactivate it
                    EventAggregator.GetEvent<BarCheckItemResetStateEvent>().Publish(string.Empty);
                }
    ...
}

Now the problem is, I can measure the room one time properly, but when the popup opens back up after measuring and I try to measure the room again I can’t create any polygon nodes anymore. Also, if I deactivate the room measuring via the button in the ribbon bar I can’t create any more room nodes, or select and move any node (double click still works).
I suspect the problem is caused by calling the measure room size button still within the OnSelectionChanged method, thus the OnSelectionChanged still being active while I actually remove it from the diagram when deactivating the room measuring, and thus still being active when I reopen the popup and still being active when I click the measure room size button in the popup a second time and still being active when the OnSelectionChanged handler gets added back to the diagram. And only then it steps out of the method. So this might cause issues, but I’m not sure how to handle this problem. What I actually need is a method called after the OnSelectionChanged method has finished when the polygon was closed. I’ve tried creating a transaction and listening to model changes but ModelChanged never gets thrown when I call CommitTransaction("MeasureRoomSize"). I’ve also tried listening to LayoutCompleted but it’s being called so often and I have no idea how to identify that one single call from my CommitTransaction("MeasureRoomSize").

What can I do?

I don’t think I’ve ever seen such a comprehensive post.

Yes, all SelectionChanged event handlers need to be careful to be short and not modify the selection collection.

I think you might need to implement some kind of state machine and have events result in state transitions rather than calling functionality synchronously.

It took me a while but it’s a complex problem and finding out the cause of it kept me busy long enough that this post didn’t add much to it so I thought it’d be better to put some effort in writing it down rather than having to describe the problem multiple times because of a lack of information and misunderstandings :)

I see. Never implemented a state machine but I’ll have a look into it, thanks for the suggestion.

I checked on how to implement a state design pattern and now I’m wondering what benefit that’ll bring. The diagram itself is defined in the .xaml and will be passed to the view model. This would then be passed on to the state, which adds/removes the event handlers. The state transitions would be called within event handlers of the state, which upon transition would react with adding/removing the event handlers from the same diagram as every state works with one instance of a diagram. All I gained was another layer of abstraction between the event handler and the modification of the handlers. This wouldn’t be the case if every state had his own diagram to work with but as that is defined in the xaml, I’m not sure how I could get a new diagram for every state.

It’s important not to confuse the selection mechanisms of GoXam by changing the selection collection within a SelectionChanged event handler. I was thinking that you could queue up a request to change the “mode” of the application (enabling/disabling/replacing various tools and commands), rather than making those changes synchronously within the event handler.

The reason I was suggesting organizing your app to use a state machine was to make it clear what state the app is in at any time, and to make state transitions where you reenable/replace tools and commands. All of these state changes would only happen outside of any SelectionChanged or Click event handler, so that you could avoid any issues with trying to execute code within any UI event handler.

I’m sorry if I’m not making myself very clear. I was not suggesting that you would have multiple Diagrams or Models or anything, unless you really wanted them. I was just suggesting one way to avoid changing the tools and commands and event handlers within any UI event handler (including in modal dialogs), since that appears to be the problem that you are having.

I am really stuck on how to implement this. I’ve created states to switch and implemented an EventQueue class with an ObservableQueue inside it, which throws a collection changed event when enqueuing an Action and upon this starts to execute the enqueued action. However, all this is still called synchronously and as the state change is requested in the OnSelectionChanged event handler all of it happens while the OnSelectionChanged is still active.

I thought about using another Task when adding an action to my event queue but I’d like to avoid any threading issues and even if, I’d still need to ensure that the OnSelectionChanged has actually finished and has been exited.

You are right that the problem is due to your synchronously making state changes.

I was just suggesting that your SelectionChanged event handlers call myDiagram.Dispatcher.BeginInvoke.

Ah … I always forget about that. Now that I’ve rewritten the states using the diagram’s dispatcher for the state changes it finally works :) Thank you.