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 […]
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):
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
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
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));
}