Using Enterprise Architect Add-In Search Methods with XmlSerializer

Sparx Systems Enterprise Architect supports the extension of the built-in search functionality by creating search queries with add-ins called Add-In Search. In this article I show you how to create an Add-In search that returns results to the “Find in Project” dialogue using an XML serialiser.

  • Add-In Search Methods
  • XML structure
  • Serialisation

Add-In Search Methods

Add-In Searches are public methods of regular add-ins that have the following signature:

public object GetAllElements(Repository repository, string searchText, ref string xmlResults)

The method must reside in the Add-In and return true or any non-null object and pass the search result via the ref xmlResults parameter back to the caller.

public sealed class EntryPoint
{
  public object GetAllElements(Repository repository, string searchText, ref string xmlResults)
  {
    var elements = repository
      .GetElementSet("SELECT * FROM t_objects", 2)
      .ToList();
    xmlResults = new ReportViewData()
      .Serialise(elements, ValueSelector, PropertyNamesSelector);
    return true;
  }
}

I give you more details on the ReportViewData class and its Selectors later.

XML structure

The XML structure is a little bit awkward and untyped (you find documentation here):

<ReportViewData>
  <Fields>
    <Field name="Name" />
    <Field name="Value" />
  </Fields>
  <Rows>
  <Row>
    <Field name="Name" value="theName1" />
    <Field name="Value" value="theValue1" />
    </Row>
  <Row>
    <Field name="Name" value="theName2" />
    <Field name="Value" value="theValue2" />
  </Row>
  </Rows>
</ReportViewData>

As one can see, the XML format uses both XML elements as well as attributes – thus we are out of luck using the DataContractSerializer which does not support XML attributes. Therefore we have to fall back to the infamous XMLSerializer with all its pros and cons.

In order to dynamically serialise that structure from C# classes we have to create the following class hierarchy:

[ XmlSerializerFormat ]
public class ReportViewData
{
  [ XmlElement(nameof(Fields)) ]
  public FieldElement Fields { get; set; } = new FieldElement();

  [ XmlElement(nameof(Rows)) ]
  public RowsElement Rows { get; set; } = new RowsElement();

  public class FieldElement
  {
    [ XmlElement(Namespace = "Fields") ]
    public List<FieldField> Field { get; set; } = new List<FieldField>();
  }

  public class RowsElement
  {
    [ XmlElement ]
    public List<RowElement> Row { get; set; } = new List<RowElement>();
  }

  public class RowElement
  {
    [ XmlElement(Namespace = "Rows") ]
    public List<RowField> Field { get; set; } = new List<RowField>();
  }

  [ XmlSerializerFormat ]
  public class FieldField
  {
    [ XmlAttribute("name") ]
    public string Name { get; set; }
  }

  [ XmlSerializerFormat ]
  public class RowField
  {
    [ XmlAttribute("name") ]
    public string Name { get; set; }

    [ XmlAttribute("value") ]
    public string Value { get; set; }
  }
}

As the Field element is used more than once in the hierarchy we have to declare a namespace, on our classes to the XMLSerialiser knows which actual class to use. This will turn into a small problem as we will discover when trying to serialise the data.

Serialisation

Now that we have a class structure let’s start with the serialisation process: We try to make our resulting XML as clean as possible and therefore do not use namespaces or XML declarations. But as we defined namespaces in our class hierarchy the XMLSerialiser correctly inserts that namespace declaration and prefixes into the resulting XML (xmlns="Fields" and xmlns=Rows"). However, this will prevent Enterprise Architect from displaying our search results. But with String.Rpeplace to the rescue we ‘clean’ the result before returning our value as you can see at the end of the method.

public string Serialise()
{
  var result = default(string);

  var emptyNamespaces = new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });
  var serializer = new XmlSerializer(this.GetType());
  var settings = new XmlWriterSettings
  {
    Indent = false,
    OmitXmlDeclaration = true,
    Encoding = Encoding.UTF8,
  };

  using (var stream = new StringWriter())
  using (var writer = XmlWriter.Create(stream, settings))
  {
    serializer.Serialize(writer, this, emptyNamespaces);
    result = stream.ToString();
  }

  // strip namespace prefixes from XML result
  return result
    .Replace(XMLNS_FIELDS, string.Empty)
    .Replace(XMLNS_ROWS, string.Empty);
}

To further faciliate the invocation of the serialiser we provide some helper methods that work with lists and dictionaries:

List Serialisation

public string Serialise<T>(IList<T> tList)
  where T : class
{
  var propertyNames = typeof(T)
    .GetProperties(BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance)
    .Select(e => e.Name)
    .ToList();
  propertyNames.ForEach(name => Fields.Field.Add(new FieldField {Name = name}));

  tList.ForEach(item =>
  {
    var row = new RowElement();
    propertyNames.ForEach(name =>
    {
      var propertyInfo = typeof(T).GetProperty(name);
      if(null == propertyInfo) return;

      var value = propertyInfo.GetValue(item, null).ToString();
      row.Field.Add(new RowField {Name = name, Value = value});
    });
    Rows.Row.Add(row);
  });

  return Serialise();
}

Dictionary Serialisation

public string Serialise(IDictionary<string, string> dictionary)
{
  const string NAME = "Name";
  const string VALUE = "Value";

  Fields.Field.Add(new FieldField {Name = NAME });
  Fields.Field.Add(new FieldField {Name = VALUE });

  dictionary.ForEach(kvp =>
  {
    var row = new RowElement();
    row.Field.Add(new RowField { Name = NAME, Value = kvp.Key });
    row.Field.Add(new RowField { Name = VALUE, Value = kvp.Value.ToString() });
    Rows.Row.Add(row);
  });

  return Serialise();
}

Serialisation of EA objects

When trying to pass a list of EA elements to the serialiser you will find out immediately that no search results are being displayed. This is because the property lookup via reflection does not work on the dynamic objects of EA. However as usual, there is help: We create an overload (similar to a Visitor) so that the caller can pass in a Func (or two) to help get the needed properties and values into the resulting XML:

public string Serialise<T>(IList<T> tList, Func<T, string, string> valueSelector, Func<T, IList<string>> propertyNamesSelector)
  where T : class
{
  var propertyNames = propertyNamesSelector.Invoke(tList.First());

  propertyNames.ForEach(name => Fields.Field.Add(new FieldField { Name = name }));

  tList.ForEach(item =>
  {
    var row = new RowElement();
    propertyNames.ForEach(name =>
    {
      var value = valueSelector.Invoke(item, name);

      row.Field.Add(new RowField { Name = name, Value = value });
    });
    Rows.Row.Add(row);
  });

  return Serialise();
}

We can then call the serialiser with something like this:

var reportViewData = new ReportViewData();
xmlResults = reportViewData.Serialise(elements, ValueSelector, 
  element => new List<string> {"CLASSGUID", "CLASSTYPE", "ElementID", "MetaType", "Name", "Author"});

private static string ValueSelector(Element element, string propertyName)
{
  Contract.Requires(null != element);
  Contract.Requires(!string.IsNullOrWhiteSpace(propertyName));

  switch (propertyName)
  {
    case "CLASSGUID":
      return element.ElementGUID;
    case "CLASSTYPE":
      return element.MetaType;
    case nameof(element.ElementID):
      return element.ElementID.ToString();
    case nameof(element.MetaType):
      return element.MetaType;
    case nameof(element.Name):
      return element.Name;
      case nameof(element.Author):
      return element.Author;
    default:
      return string.Empty;
  }
}

Putting it all together

  • Defining an Add-In Search
  • Querying and returning elements
  • Search results

Defining an Add-In Search

DefineSearch

DefineSearch

The name of the Add-In is not the name of the Add-In class but the key value in the registry under which is the Add-In is registered. Though technically an Add-In name may contain dot characters your Add-In name must not contain dots – otherwise the Add-In Search will return an error stating the search method could not be found.

Querying and returning elements

Here we perform a simple SQL query of elements in the t_objects table. The actual query resides in a resource file for maintainability. As we are only interested in certain fields/attributes we select them via the defined Func parameters (once as a delegate and once as a Lambda expression). Inside the ValueSelector we format the elements based on their MetaType to ensure that the search result is “clickable”.

public object GetAllElements(Repository repository, string searchText, ref string xmlResults)
{
  var elements = repository
    .GetElementSet(Queries.GetAllElements, 2)
    .ToList();

  var reportViewData = new ReportViewData();
  xmlResults = reportViewData.Serialise(elements, ValueSelector, element => new List<string>
    {"CLASSGUID", "CLASSTYPE", "ElementID", "MetaType", "Name", "Author"});

  return true;
}

private static string ValueSelector(Element element, string propertyName)
{
  switch (propertyName)
  {
    case "CLASSGUID":
      return element.ElementGUID;
    case "CLASSTYPE":
      return element.MetaType;
    case nameof(element.ElementID):
      return element.ElementID.ToString();
    case nameof(element.Name):
      return element.Name;
    case nameof(element.MetaType):
      return element.MetaType;
      case nameof(element.Author):
      return element.Author;
    default:
      return string.Empty;
  }
}

Search results

In this search result the elements are “clickable” as the hidden column names CLASSGUID and CLASSTYPE were returned in the XML results (the same way you would return them in a custom SQL query):

SearchResults

SearchResults

Examples

Below you find some more examples on how to use the serialisation methods.

Manual setup of the class structure

[ TestMethod ]
public void SerialiseManualSucceeds()
{
  // Arrange
  var sut = new ReportViewData();
  sut.Fields.Field.Add(new ReportViewData.FieldField());
  sut.Fields.Field.First().Name = "Name";
  sut.Fields.Field.Add(new ReportViewData.FieldField());
  sut.Fields.Field.Last().Name = "Value";

  var rowElement1 = new ReportViewData.RowElement();
  rowElement1.Field.Add(new ReportViewData.RowField { Name = "Name", Value = "theName1" });
  rowElement1.Field.Add(new ReportViewData.RowField { Name = "Value", Value = "theValue1" });

  sut.Rows.Row.Add(rowElement1);

  var rowElement2 = new ReportViewData.RowElement();
  rowElement2.Field.Add(new ReportViewData.RowField { Name = "Name", Value = "theName2" });
  rowElement2.Field.Add(new ReportViewData.RowField { Name = "Value", Value = "tralala" });

  sut.Rows.Row.Add(rowElement2);

  var result = sut.Serialise();

  // Assert
  Assert.IsTrue(result.Contains("tralala"));
}

List Serialisation

[ TestMethod ]
public void SerialiseWithListSucceeds()
{
  // Arrange
  const string EXPECTED_VALUE = "someOtherValue";
  var sut = new ReportViewData();

  var list = new List<ArbitraryItem>
  {
    new ArbitraryItem() {Name = "Peter", Value = "arbitraryValue"},
    new ArbitraryItem() {Name = "Paul", Value = "otherValue"},
    new ArbitraryItem() {Name = "Mary", Value = EXPECTED_VALUE},
  };

  // Act
  var result = sut.Serialise(list);

  // Assert
  Assert.IsTrue(result.Contains(EXPECTED_VALUE));
}

Dictionary Serialisation

[ TestMethod ]
public void SerialiseWithDictionarySucceeds()
{
  // Arrange
  const string EXPECTED_VALUE = "someOtherValue";
  var sut = new ReportViewData();

  var dictionary = new Dictionary<string, string>
  {
    {"Peter", "arbitraryValue"},
    { "Paul", "otherValue"},
    { "Mary", EXPECTED_VALUE}
  };

  // Act
  var result = sut.Serialise(dictionary);

  // Assert
  Assert.IsTrue(result.Contains(EXPECTED_VALUE));
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: