Home > Plug-in Development > How to add new contracts for the Add Contract code provider

How to add new contracts for the Add Contract code provider

February 18th, 2011

Note, this article has been moved to the official DevExpress Support Center site. Please refer to the moved article as it might have further updates or additional comments. Thank you.

The Add Contract code provider adds conditions for validation of the active method parameters. The Extensible architecture of the DXCore allows you to add new contracts to the list of available contracts using the ContractProvider component inside your plug-in.

The sample below is a rough implementation for the suggestions registered in the DevExpress Support Center database:

  • S133903 Add Contract – Use IsNullOrWhiteSpace on contracts
  • S136474 Add Contract – Allow it to generate code for single parameter

The sample is intended to illustrate how it is to do this using the DXCore yourself. The source code is attached at the end of the post in two versions – CSharp and Visual Basic. The implementation is rough, because there are some availability checks that are missing. For example, there’s no availability check that the contract already exists. Also, the new contract doesn’t work for all parameters simultaneously – it is available only for a single parameter. However, you are welcome to extend it and make it work as you wish in any possible way.

We are going to add a contract available on a single parameter of a string type and that generates a contract that throws an ArgumentException if the parameter value doesn’t satisfy the condition (string is null or contains only a whitespace).

Follow these steps to get started:

  • Create a new DXCore plug-in if you haven’t done so yet.
  • Drop the ContractProvider control on the plug-in designer surface. Please read about this component to learn how it can be added into your Toolbox.
  • Fill the ProviderName property of the ContractProvider component (e.g., “IsNullOrWhiteSpace“)
  • Set the AutoUndo property to True. This is needed for the undo mechanism to work correctly, so you don’t have worry about it.

DXCore ContractProvider properties

  • Create event handlers for the CheckAvailability and Apply events by double clicking on them in the Properties tool window:

DXCore ContractProvider events

Next, we’ll write the code. There are two parts of code: the first one – CheckAvailability event handler – checks the availability of the contract provider; it may or not be available. If it is not available, the contract won’t appear in the list of contracts for the Add Contract code provider. If it is available, the second part of code will apply the contract; in other words, it will generate and paste the appropriate code into the method body.

The CheckAvailability event handler should check if it is OK to make the contract available in the current context. The context consists of an active code element, caret position, selection state, etc. For this particular contract, we have to check the following conditions:

  1. The current code element is a parameter;
  2. The parameter is a method parameter;
  3. The parameter is of a type string.

If all requirements are satisfied, then we can tell that the contract is available by setting the appropriate property; see the code below:

private void contractProvider1_CheckAvailability(object sender, CheckContentAvailabilityEventArgs ea)
{
  // See if we are at the parameter element...
  Param activeParam = GetActiveParamElement(ea.Element);
  if (activeParam == null)
    return;

  // See if the parameter is a method parameter...
  if (activeParam.GetParentMethod() == null)
    return;

  // Check if the parameters is of string type...
  if (!activeParam.Is("System.String"))
    return;

  // Now, when all checks are done, tell that the provider is available...
  ea.Available = true;
}

Show Visual Basic code »

Private Sub ContractProvider1_CheckAvailability(ByVal sender As System.Object, ByVal ea As DevExpress.CodeRush.Core.CheckContentAvailabilityEventArgs) Handles ContractProvider1.CheckAvailability
  ' See if we are at the parameter element...
  Dim activeParam As Param = GetActiveParamElement(ea.Element)
  If activeParam Is Nothing Then
    Return
  End If

  ' See if the parameter is a method parameter...
  If activeParam.GetParentMethod() Is Nothing Then
    Return
  End If

  ' Check if the parameters is of string type...
  If Not activeParam.Is("System.String") Then
    Return
  End If

  ' Now, when all checks are done, tell that the provider is available...
  ea.Available = True
End Sub

Here’s the code of the GetActiveParamElement method, it returns the current parameter element if possible:

private Param GetActiveParamElement(LanguageElement languageElement)
{
  if (languageElement == null)
    return null;

  Param param = languageElement as Param;
  if (param == null)
    param = languageElement.GetParent(LanguageElementType.Parameter) as Param;

  return param;
}

Show Visual Basic code »

Private Function GetActiveParamElement(ByVal languageElement As LanguageElement) As Param
  If languageElement Is Nothing Then
    Return Nothing
  End If

  Dim param As Param = TryCast(languageElement, Param)
  If param Is Nothing Then
    param = TryCast(languageElement.GetParent(LanguageElementType.Parameter), Param)
  End If

  Return param
End Function

Now the second part, the Apply event handler. Here we have to make the following steps:

  1. Retrieve the current parameter, which was checked for availability of the contract. It is needed for a generation of the resulting code of the contract.
  2. Generate the code, taking into account the parameter.
  3. Find the source point where we will insert the generated code
  4. Insert the contract code

For retrieving the parameter, we can use the same method we used in the CheckAvailability method – GetActiveParameterElement.

private void contractProvider1_Apply(object sender, ApplyContentEventArgs ea)
{
  // Step 1 - retrieve the active parameter...
  Param activeParam = GetActiveParamElement(ea.Element);
  if (activeParam == null) // sanity check - should never happen...
    return;

  // Step 2 - generate the contract code...
  string contractCode = GetContractCode(activeParam);

  // Step 3 - find a point where the generated code will be inserted...
  SourcePoint insertionPoint = GetInsertionPoint(activeParam);

  // Step 4 - Insert the contract code..
  CodeRush.TextExpansions.Insert(ea.TextDocument,
                                  insertionPoint,
                                  contractCode,
                                  false /* allowCancel */,
                                  true /* format */);

  SourceRange codeRange = ea.TextDocument.InsertText(insertionPoint, contractCode);
  ea.TextDocument.Format(codeRange);

  // Additional step - declare System namespace reference ("using System;") at the top of the file if it is not declared yet...
  CodeRush.Source.DeclareNamespaceReference("System");
}

Show Visual Basic code »

Private Sub ContractProvider1_Apply(ByVal sender As System.Object, ByVal ea As DevExpress.CodeRush.Core.ApplyContentEventArgs) Handles ContractProvider1.Apply
  ' Step 1 - retrieve the active parameter...
  Dim activeParam As Param = GetActiveParamElement(ea.Element)
  If activeParam Is Nothing Then ' sanity check - should never happen...
    Return
  End If

  ' Step 2 - generate the contract code...
  Dim contractCode As String = GetContractCode(activeParam)

  ' Step 3 - find a point where the generated code will be inserted...
  Dim insertionPoint As SourcePoint = GetInsertionPoint(activeParam)

  ' Step 4 - Insert the contract code..
  CodeRush.TextExpansions.Insert(ea.TextDocument,
                                  insertionPoint,
                                  contractCode,
                                  False,
                                  True)

  ' Additional step - declare System namespace reference ("Imports System") at the top of the file if it is not declared yet...
  CodeRush.Source.DeclareNamespaceReference("System")
End Sub

The GetContractCode method, which generates the code, is made up of tricky parts. We are going to generate the code like this:

if (String.IsNullOrWhiteSpace(stringParameter))
  throw new ArgumentException("stringParameter is null or whitespace.", "stringParameter");

Show Visual Basic code »

If String.IsNullOrWhiteSpace(stringParameter) Then
  Throw New ArgumentException("stringParameter is null or whitespace.", "stringParameter")
End If

Here, we have to construct every piece of this code. You can take a look at what it structurally looks like using the Expression Lab DXCore tool window. There are around ten pieces we have to create, you can see them in the source tree:

DXCore Contract code structure

The source code for generating that code may look like this:

private static string GetContractCode(Param activeParam)
{
  MethodReferenceExpression methodReferenceExpression = new MethodReferenceExpression("String.IsNullOrWhiteSpace");
  MethodCallExpression methodCallExpression = new MethodCallExpression(methodReferenceExpression);
  methodCallExpression.Arguments.Add(new ElementReferenceExpression(activeParam.Name));

  Throw throwStatement = new Throw();
  ExpressionCollection throwStatementArguments = new ExpressionCollection();
  throwStatementArguments.Add(new SnippetExpression(CodeRush.StrUtil.AddQuotes(activeParam.Name + " is null or whitespace.")));
  throwStatementArguments.Add(new SnippetExpression(CodeRush.StrUtil.AddQuotes(activeParam.Name)));
  ObjectCreationExpression objectCreationExpression = new ObjectCreationExpression(new TypeReferenceExpression("ArgumentException"), throwStatementArguments);
  throwStatement.Expression = objectCreationExpression;

  LanguageElementCollection nodes = new LanguageElementCollection();
  nodes.Add(throwStatement);
  If ifStatement = new If(methodCallExpression, nodes);

  return CodeRush.Language.GenerateElement(ifStatement);
}

Show Visual Basic code »

Private Shared Function GetContractCode(ByVal activeParam As Param) As String
  Dim methodReferenceExpression As MethodReferenceExpression = New MethodReferenceExpression("String.IsNullOrWhiteSpace")
  Dim methodCallExpression As MethodCallExpression = New MethodCallExpression(methodReferenceExpression)
  methodCallExpression.Arguments.Add(New ElementReferenceExpression(activeParam.Name))

  Dim throwStatement As [Throw] = New [Throw]()
  Dim throwStatementArguments As ExpressionCollection = New ExpressionCollection()
  throwStatementArguments.Add(New SnippetExpression(CodeRush.StrUtil.AddQuotes(activeParam.Name + " is null or whitespace.")))
  throwStatementArguments.Add(New SnippetExpression(CodeRush.StrUtil.AddQuotes(activeParam.Name)))

  Dim objectCreationExpression As ObjectCreationExpression = New ObjectCreationExpression(New TypeReferenceExpression("ArgumentException"), throwStatementArguments)
  throwStatement.Expression = objectCreationExpression
  Dim nodes As LanguageElementCollection = New LanguageElementCollection()
  nodes.Add(throwStatement)

  Dim ifStatement As [If] = New [If](methodCallExpression, nodes)
  Return CodeRush.Language.GenerateElement(ifStatement)
End Function

After code is generated, we need to find out the point where it will be inserted. The contract is going to be inserted at the start of the method.

private SourcePoint GetInsertionPoint(Param activeParam)
{
  SourcePoint result = SourcePoint.Empty;
  Method parentMethod = activeParam.GetParentMethod();
  if (parentMethod == null)
    return result;

  if (parentMethod.NodeCount > 0)
    result = (parentMethod.Nodes[0] as LanguageElement).Range.Start;
  else
    CodeRush.Language.GetCodeBlockStart(parentMethod, ref result);

  return result;
}

Show Visual Basic code »

Private Function GetInsertionPoint(ByVal activeParam As Param) As SourcePoint
  Dim result As SourcePoint = SourcePoint.Empty
  Dim parentMethod As Method = activeParam.GetParentMethod()
  If parentMethod Is Nothing Then
    Return result
  End If

  If parentMethod.NodeCount > 0 Then
    result = CType(parentMethod.Nodes(0), LanguageElement).Range.Start
  Else
    CodeRush.Language.GetCodeBlockStart(parentMethod, result)
  End If

  Return result
End Function

In this code, we take the method of the parameter and see if it has any nodes (e.g., other code) inside. If there are any nodes, we take the starting point of the first node. Otherwise, we use the GetCodeBlockStart method of the Language service to calculate the target point.

The final task is to insert the generated code to the target point:

CodeRush.TextExpansions.Insert(ea.TextDocument,
  insertionPoint,
  contractCode,
  false /* allowCancel */,
  true /* format */);

This is a special call which is supposed to insert complex text expansions. Instead of this call, we could use the “InsertText” method of the ea.TextDocument object, but it doesn’t format the resulting code. In this case, we would have to call the additional “Format” method on the ea.TextDocument object like this:

SourceRange codeRange = ea.TextDocument.InsertText(insertionPoint, contractCode);
ea.TextDocument.Format(codeRange);

The first way is preferred if you are going to add any text commands in the generated call. For example, using text commands, you can position the text caret, and for example, add text fields for the arguments of the ArgumentException call to add the capability to enter new values after the contract is added.

After the project is compiled, we can test the new contract. Place the caret on a string parameter, and apply it:

DXCore Contract preview

Result:

DXCore Contract result (CS)

Visual Basic:

DXCore Contract result (VB)

Here is the attached source code (Visual Studio 2010, 34666 bytes). You can use it in any way.

—–
Products: DXCore
Versions: 12.1 and up
VS IDEs: any
Updated: Nov/01/2012
ID: D062

Similar Posts: