[Из песочницы] Простой генератор DGML-файла графа переходов машины состояний
Допустим, есть проект WPF/MVVM, в котором необходимо реализовать шаблон State Machine, позволяющий управлять поведением объекта (в данном случае, ViewModel) в зависимости от того состояния, в котором он находится. При этом необходимо получить простую реализацию этого шаблона без использования Windows Workflow Foundation, которая включала бы в себя классы состояний, класс реализующий логику переходов и таблицу переходов. И наряду с вопросами реализации этого шаблона стоит задача реализации инструмента, автоматизирующего процесс построения диаграммы состояний на основе таблицы переходов. При этом граф, построенный с помощью этого инструмента, должен отвечать следующим требованиям:
- граф должен обладать понятной и упорядоченной визуальной структурой (ручное упорядочивание вершин и связей графа должно быть сведено к минимуму);
- файл графа должен быть включен в проект и, соответственно, в систему контроля версий;
- вершина графа должны обладать кликабельной ссылкой на файл, в котором реализовано состояние;
- должна быть реализована возможность задавать стили к вершинам графа.
Так, если про реализацию паттерна машины состояний в контексте проекта WPF/MVVM есть достаточно материала, то для решения второй задачи – реализации генератора графа переходов – очевидного решения не нашлось. Но при анализе материала на эту тему я наткнулся на эту статью, которая меня и натолкнула на решение. Так, в этой статье автор вручную формирует граф состояний с помощью инструмента Visual Studio, а именно визуального редактора DGML-файлов (Direct Graph Markup Language), и далее, на основе полученного графа, программно формирует таблицу переходов машины состояний.
DGML-файл (файл ориентированного графа) имеет XML представление, структура которого отлично описана в MSDN. Так, программно редактируя XML представление можно изменить визуальное представление графа. Таким образом, был выбран инструмент визуализации графа, осталось реализовать генератор, который на основе имеющейся таблицы переходов формировал бы XML представление DGML-файла.
Так было принято решение добавить DGML-файл в решение проекта и реализовать генератор графа в тестовом методе:
[TestMethod]
public void ClientStateMachineTest()
{
// Экземпляр машины состояний ClientStateMachine
var clientStateMachine = new ClientStateMachine();
var xmlDoc = new XmlDocument();
// Относительный путь до DGML-файла, включенного в решение проекта
const string fileDgml = @"..\..\SM\Test\ClientStateMachineGraph.dgml";
xmlDoc.Load(fileDgml);
var nodeLinks = xmlDoc.SelectSingleNode("/*[local-name()='DirectedGraph']/*[local-name()='Links']");
var nodes = xmlDoc.SelectSingleNode("/*[local-name()='DirectedGraph']/*[local-name()='Nodes']");
if (nodes != null)
{
nodes.RemoveAll();
foreach (var state in clientStateMachine.StatesCollection)
{
var newNode = xmlDoc.CreateNode(XmlNodeType.Element, "Node", "http://schemas.microsoft.com/vs/2009/dgml");
var id = xmlDoc.CreateAttribute("Id");
id.Value = state.GetType().Name;
var reference = xmlDoc.CreateAttribute("Reference");
reference.Value = string.Format(@"..\..\SM\States\{0}.cs", state.GetType().Name);
var background = xmlDoc.CreateAttribute("Background");
background.Value = state.Background.Name;
if (newNode.Attributes != null)
{
newNode.Attributes.Append(id);
newNode.Attributes.Append(background);
newNode.Attributes.Append(reference);
}
nodes.AppendChild(newNode);
}
}
if (nodeLinks != null)
{
nodeLinks.RemoveAll();
foreach (var tr in clientStateMachine.Transitions)
{
var newLink = xmlDoc.CreateNode(XmlNodeType.Element, "Link", "http://schemas.microsoft.com/vs/2009/dgml");
var source = xmlDoc.CreateAttribute("Source");
source.Value = (tr.Value.InitialState).GetType().Name;
var target = xmlDoc.CreateAttribute("Target");
target.Value = tr.Value.FinalState.GetType().Name;
if (newLink.Attributes != null)
{
newLink.Attributes.Append(source);
newLink.Attributes.Append(target);
}
nodeLinks.AppendChild(newLink);
}
}
xmlDoc.Save(fileDgml);
}
В начале метода на основе относительного пути к DGML-файлу проекта загружается XML-документ, из которого извлекаются XML-узел Links, содержащий ориентированные связи графа Link, и XML-узел Nodes, содержащий вершины графа Node.
Далее, на основе коллекции состояний clientStateMachine.StatesCollection формируются вершины графа, у которых устанавливаются ссылки на файлы состояний и цвет фона.
Затем, на основе каждого перехода из таблицы переходов clientStateMachine.Transitions, имеющего начальное InitialState и конечное FinalState состояния, формируется направленное ребро графа путем добавления соответствующих атрибутов Source и Target в XML-элемент Link.
Результат выполнения этого тестового метода представлен на рисунке ниже.
В заключение, хочу отметить, что:
- наглядная структура графа, без наложения или пересечения вершин и связей, была получена автоматически с помощью конструктора макета графа, что является отличным преимуществом данного инструмента Visual Studio;
- перейти по ссылке к файлу состояния можно из контекстного меню вершины графа;
- представленный генератор можно легко адаптировать под любую машину состояний, имеющую таблицу переходов.
Таким образом, представлена простая, но эффективная реализация генератора ориентированного графа в тестовом методе, выполнение которого позволяет получить актуальную версию диаграммы состояний.