This posting is part of a series detailing how to write a custom Debugger Visualizer for Visual Studio.
Posts in the series:
- Writing A (ReadOnly) Custom Debugger Visualizer for Visual Studio
- Writing A Custom Debugger Visualizer for Non-Serializable Types or More Complex Objects
- Writing A Custom Debugger Visualizer using WPF for the UI
- Modifying The Live Object in Your Custom Debugger Visualizer for Visual Studio
- Installation, Limitations, and Security Considerations for Your Custom Debugger Visualizer for Visual Studio
I've also given a talk on this topic: Putting the Visual into the Visual Studio Debugger.
You can find code samples to go along with this series in my DebugVisualizer
GitHub repository.
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 yourVisualizerObjectSource.TransferData
, upon decoding the message, would callResponse.GetResponseStream()
and read the response into astring
object. Then, that string could be returned on the outgoingStream
. Then, the visualizer would receive thestring
as the return value of it's call toTransferData
, 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.