Modifying The Live Object in Your Custom Debugger Visualizer for Visual Studio

This post builds upon the first post in the series Writing A (ReadOnly) Custom Debugger Visualizer for Visual Studio. Please at least skim through that post to understand the topics in this one.

Making Your Visualizer Editable

There are cases where just viewing data from the object you're visualizing during a debugging session isn't enough. You'd like to modify the object in some way. Perhaps to setup a specific failure/success scenario or replicate a bug/bug fix.

If you're using a read-only visualizer, like what we've built so far in this series, you'd need to close the visualizer and use the Immediate Window to modify the data. Particularly if the value you want to modify is deeply nested in your object graph, or you have a bunch of items in a child collection and finding the right one to modify is tedious, then using the Immediate Window can be an exercise in frustration.

Luckily, the Debugger Visualizer API allows a great amount of flexibility to modify the object we're visualizing.

The Components Involved

Going from a read-only visualizer to one that can modify/interact with the underlying object means taking advantage of additional methods in the IVisualizerObjectProvider and VisualizerObjectSource.

Specially, in IVisualizerObjectProvider, you can call ReplaceData/ReplaceObject or TransferData/TransferObject to request changes to the live object. Just like the GetData/GetObject calls we discussed in earlier posts, the "Data" versions of the methods work against Stream objects and you must serialize/deserialize the values yourself, while the "Object" methods will call the "Data" method, then automatically serialize/deserialize for you.

Calls to IVisualizerObjectProvider.ReplaceData result in a call to VisualObjectSource.CreateReplacementObject on the app side. The intention of this API is to completely replace the live object with the version being passed into the ReplaceData call. If the object being visualized is Serializable, you can use the default implementation of VisualizerObjectSource.CreateReplacementObject, which will deserialize the incoming Stream and return the object as the new version. Otherwise, you'll need to write your own class that extends VisualizerObjectSource and overrides CreateReplacementObject to take in the Stream coming from the Visual Studio side (which could be a ViewModel object if the primary object isn't serializable) and convert it into an instance of the Type being visualized to replace the current object.

Additionally, you'll need to first verify that IVisualizerObjectProvider.IsObjectReplaceable is true. Otherwise, you won't be allowed to manipulate the object via the ReplaceData method. Documentation on when this would be false is scarce, but I did find this summary block for the property in a Patent filing for this feature:

public interface IVisualizerObjectProvider
{
    /// <summary>
    /// Specifies whether a replacement object can be created. For instance, if the object being
    /// visualized is a readonly field then this will return false.
    /// </summary>
    /// <returns></returns>
    bool IsObjectReplaceable { get; }

From https://www.google.com/patents/US7657873

Alternatively, you can use the TransferData method to send any arbitrary data stream from your visualizer to the VisualizerObjectSource.TransferData method. With this pair of methods, you can send serialized data from the Visual Studio side to the application side, and optionally have it reply back with a serialized object. This opens up the possibly of implementing a messaging system between the two side for more complex interactions.

For instance, in the case of a WebException, you could decide to use the actual WebException type in the GetData call, then, should the user click a button on the UI to review the response HTML, you could send a message to the application via the TransferData call, have it read the WebExceptionResponse.GetResponseStream() Stream and return the string version of the output.

NOTE: Communication is always initiated by the Visual Studio side -- specifically, calls from your Visualizer class into IVisualizerObjectProvider. The application-side cannot initiate the conversation and TransferData is the only method where it can return something other than the object being used by your visualizer.

The Logical Flow (For Modifying an Object from Your Visualizer)

The initial flow for getting data into your visualizer will remain the same (see the previous posts in this series). But once visualized, users can then interact with your UI to initiate changes to the object. Generally, this will be via OnChange-type events in your object/ViewModel or button click UI events that are handled by your Visualizer class. Thus, any changes or actions the user makes via the UI will be picked up by your Visualizer class.

Then, from your handler, you need to decide which API you want to use.

The simpler path would be to modify the version of the object you already have in memory (or have the UI bound directly to the object so it is automatically updated on changes), then call ReplaceObject with the updated object.

Otherwise, you can call TransferObject with some object of your choosing, such as a messaging DTO you've created that holds the action or changes you want to pass to the application.

This, in turn, will call the corresponding method in VisualizerObjectSource.

If you used ReplaceData or ReplaceObject, your implementation of CreateReplacementObject is called with the current object instance and the Stream of the serialized object coming from the IVisualizerObjectProvider. That method will need to take the incoming Stream and return an object. When the method returns the object, it will replace the original object in the applications memory.

If you used TransferData or TransferObject, your implementation of TransferData will be called. You cannot use the base implementation of TransferData -- it will just throw NotImplementedException). There are three parameters passed into VisualizerObjectSource.TransferData:

  • The original object being visualized
  • An incoming Stream for the data being passed from the visualizer TransferData
  • An outgoing Stream to allow a reply back to TransferData

The key for using TransferData is to have both sides of the call to have a common understanding of the types being passed around so that they can be deserialized/cast to the appropriate object Type rather than just object. Then it's just a matter of determining what data needs to be modified or what action needs to be taken and acting accordingly. Ultimately, the TransferData method needs to modify the passed-in original object if changes are needed.

In that case where TransferData is used, the third parameter passed into the VisualizerObjectSource.TransferData method is an outgoing Stream. This allows for a response to be returned to the Visualizer via the IVisualizerObjectProvider.TransferData's return value. For TransferData, the return value is a Stream which you must deserialize yourself, or if you used TransferObject, it's an object that you must cast to the appropriate Type. So again, having an common understanding of the types being transferred is key.

An example of how this might be used:

For a WebResponse visualizer, if you need the actual response value, you could call IVisualizerObjectProvider.TransferData with a message object requesting the actual response value. Then your VisualizerObjectSource.TransferData, upon decoding the message, would call Response.GetResponseStream() and read the response into a string object. Then, that string could be returned on the outgoing Stream. Then, the visualizer would receive the string as the return value of it's call to TransferData, and update the UI to display the string value.

Implementing a Read-Write Visualizer

Starting from the visualizer code at the end of my "Writing A (ReadOnly) Custom Debugger Visualizer for Visual Studio" post (which used a WinForms-based UI), I'll change a few things in the VisualizerObjectSource and Visualizer implementations.

The VisualizerObjectSource

TIP: If you use ReplaceData/ReplaceObject and you're passing the actual type being visualized (not a ViewModel), then you don't actually need to implement your own VisualizerObjectSource and/or override CreateReplacementObject, as the base implementation will just deserialize the passed-in object and replace the live object with the new version.

Otherwise, you'll override the CreateReplacementObject method to handle the conversion process. If you're passing a ViewModel into TransferData, then CreateReplacementObject will need to map the ViewModel's values into either a new instance of the type being visualized, or modify the original object (which is passed in as a parameter). Ultimately, the CreateReplacementObject call must return an object which will replace the live object being visualized in the application's memory.

WARNING: When using the ReplaceData calls, be aware of object pointers in your object and make sure you wire the pointers back up correctly. The serialization process may result in new objects being created as clones of your original objects and you'll need to use the CreateReplacementObject method to replace those with the original objects pointed to in your object graph.

    public override object CreateReplacementObject(object target, Stream incomingData)
    {
        var originalObject = target as SimpleExample.SomeType;

        var incomingChangedObject = Deserialize(incomingData) as SimpleExample.SomeType;
        // if this is a ViewModel, you'll need to map the value into either a new instance of 
        // the visualized type, or modify the originalObject with the ViewModel's values and return
        // the originalObject as the new instance.

        // Beware object pointers! If you have an object graph or other data types which will result in
        // cloned objects being created during the serialization/deserialization process, you'll need to
        // handle that here.

        //returns a instance of the object, which VS will substitute for the original object in memory
        return incomingChangedObject;           
    }

If you've used the TransferData/TransferObject, you must override the VisualizerObjectSource.TransferData method. As parameters, the method will receive the original object being visualized, the incoming Stream being sent from the IVisualizerObjectProvider, and an outbound Stream which can be used to send response messages/data back to the Visualizer.

    public override void TransferData(object target, Stream incomingData, Stream outgoingData)
    {
        var originalObject = target as SimpleExample.SomeType;

        var incomingChangedObject = Deserialize(incomingData) as SimpleExample.SomeType;

        // any changes to the object must be applied to the incoming target object instance
        originalObject.Foo = incomingChangedObject.Foo;

        //(optional) send a response message back to the Visualizer
        Serialize(outgoingData, "It worked!");            
    }

The Visualizer

For the visualizer in this simple example, I'm registering an OnChange event handler for the TextBox displaying the Foo property (the only property shown in this example) and I've removed the Readonly attribute on the textbox to allow the user to edit the value. I'm using in-line delegates as my handler, but you could just as well register explicit methods.

I'm including both approaches in this code sample, but you'll want to use one or the other, not both.

    protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
    {
        if (windowService == null)
            throw new ArgumentNullException(nameof(windowService));

        if (objectProvider == null)
            throw new ArgumentNullException(nameof(objectProvider));

        // Get the object to display a visualizer for.
        //       Cast the result of objectProvider.GetObject() 
        //       to the type of the object being visualized.
        var data = objectProvider.GetObject() as SimpleExample.SomeType;


        // Display your view of the object.        
        using (var displayForm = new SomeTypeVisualizerForm(data))
        {
            displayForm.txtFoo.Text = data.Foo;


            // Read-Write Approach 1: Using ReplaceObject
            displayForm.OnChange += (sender, newObject) => objectProvider.ReplaceObject(newObject);

            // Read-Write Approach 2: Using TransferData
            displayForm.OnChange += (sender, newObject) =>
            {
                var response = objectProvider.TransferObject(newObject) as string;
                if (!string.IsNullOrEmpty(response))
                {
                    MessageBox.Show(response, "Response", MessageBoxButtons.OK);
                }

            };

            windowService.ShowDialog(displayForm);
        }
    }

Make sure to take a look at the other posts in my Debugger Visualizer series to improve upon your visualizer.