65.9K
CodeProject 正在变化。 阅读更多。
Home

V.A.P.O.R.ware - 视觉辅助编程/组织表示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (22投票s)

2016 年 11 月 18 日

CPOL

10分钟阅读

viewsIcon

25316

现在来点不一样的!

目录

引言

好的,真正的项目名称是 FlowSharpCode,但我以为我会玩一下标题!

首先,这纯粹是一个概念性的作品。它旨在启发灵感,或者引起“多么疯狂的想法”的翻白眼反应。在我看来,它是疯狂的,但在某种意义上,它是疯狂的,因为在我迄今为止有限的使用场景中,它确实有效。而且因为它如此疯狂,这篇文章的大部分内容将不会被写出来,而是应用程序使用中的截图。 这种方法的缺点是它最终看起来有点像 PowerPoint 演示文稿,这是无法避免的!

它是什么?

为什么?

它不是什么?

Hello World 示例

让我们构建一个简单的 Web 服务器!

从任何地方开始,我们将从 Main 开始

运行它!

从你的代码形状创建 DLL

现在,当你构建“应用程序”时,你有一个单独的程序集

与工作流的乐趣

让我们来看这段简单的代码

public static void Main()
{
  try
  {
    WebServer ws = new WebServer();
    ws.Start("localhost", new int[] {8001});
    Console.WriteLine("Press a key to exit the server.");
    Console.ReadLine();
  }
  catch(Exception ex)
  {
    Console.WriteLine(ex.Message);
    Console.ReadLine();
  }
}

并将其制成工作流。

定义工作流包

回到图片...

创建工作流 - 你不需要编写这段代码

但你需要编写这些

工作流的第一步

工作流的第二步

工作流的第三步

工作流的最后一步

并将旧代码更改为这样

现在,这很疯狂,很酷,有很多打字(除了复制粘贴形状,这会带着代码,使它相当痛苦),当然也无法提前退出工作流。算了——这是一个游乐场,不是火箭。

然后它就运行了

加上所有的注释等。

现在,我可以让你厌烦地用 Web 服务器程序集中的其他方法来构建工作流,但我将把它留给读者作为练习,哈哈。

到目前为止,你所看到的实际上不过是使用部分类将方法分离到离散文件中的“技巧”。没错,你在编辑器中为选定的形状编写的代码被编译为一个离散文件——它实际上被保存为一个临时文件,然后被编译,然后被删除。构建一个程序集实际上不过是弄清楚“Assy”形状中包含哪些形状,并将它们编译成一个程序集而不是一个 EXE。我们将使用类似的技术来定义工作流,但是

  • 这是你,作为程序员,第一次被限制使用特定的编码模式,
  • 而 FlowSharpCode 实际上为你生成了一些代码。

有一些限制

  • 工作流名称必须是 [packetname]workflow.dll
  • 每个工作流步骤的形状文本必须与实现的方法名相同——我使用形状文本而不是解析 C#。令人畏惧。
  • 自动生成的代码始终创建一个 Execute 方法,这是你工作流的入口点。

严格来说,这个工具的目的是将程序员限制在特定的编码模式中,并且通过适当的代码解析、类型推断和其他技巧,这些限制有可能在某个时候被放宽。

为工作流创建一个程序集

就像以前一样

  1. 将工作流包含在“Assy”形状中
  2. 给它一个文件名(以 DLL 结尾)
  3. 修复引用

 

获取代码

首先,代码库有点混乱,因为我一直在进行这些概念的原型开发,你不应该将此视为一个可用的工具。它是一个游乐场,仅此而已!从 GitHub 这里获取代码。演示文件位于

  • Article\SimpleServer.fsd - 服务器和应用程序作为一个单独的 exe 的版本
  • Article\SimpleServerAssy.fsd - 作为单独 dll 的服务器版本
  • Article\SimpleServerWorkflow.fsd - 以工作流实现的版本。
  • Article\SimpleServerWorkflowAsAssembly.fsd - 工作流也是程序集版本。

幕后

好的,这是认真的时候了。:)

首先,这个游乐场是基于我的 FlowSharp 图形工具构建的,所以我要展示的主要内容是构建 exe 和 dll 的代码,我想再次重申,这是原型“拼凑”的代码。读者可能还对以下内容感兴趣:我正在使用 SharpDevelop 的 WPF Avalon 编辑器进行 C# 代码编写,以及 Luke Buehler 的 NRefactory-Completion-Sample 用于智能感知功能,以及 ScintillaNET 编辑器用于 C# 以外的其他文件类型。整个架构基于我的模块化方法,这在我关于“The Clifton Method”的文章中有讨论。最后,停靠管理器由 DockPanelSuite 提供,最初由 WeiFen Luo 编写,现在在 GitHub 上由其他人维护。

IDE

它是完全可停靠的,但我还没有在启动时恢复你的停靠设置(还不行!)

整体架构

有很多活动的组件,因为我想要一个高度模块化的系统来开发应用程序。这是一个小小的起步,但我最好一开始就模块化,对吧?

有五个核心服务

  1. WPF Avalon 编辑器服务,由 SharpDevelop 提供
  2. Scintilla 编辑器服务
  3. 停靠窗体服务
  4. FlowSharp 画布服务
  5. FlowSharp 工具箱服务
  6. 语义处理器服务

这些都在运行时加载。你可以阅读关于如何

(是的,我喜欢在文章中看到我的名字四处飞溅。)

我在本文的其他地方提供了我正在使用的出色组件的链接,即停靠管理器、智能感知、WPF Avalon 编辑器和 ScintillaNet。

代码编译

因此,核心增值部分实际上是代码编译。它很丑陋,它在 MenuController2.cs 中(很好的地方,对吧?),所以请不要忘记它是原型代码!

顶级方法是 toaster。它做了一切

protected void Compile()
{
  tempToTextBoxMap.Clear();

  List<GraphicElement> compiledAssemblies = new List<GraphicElement>();
  bool ok = CompileAssemblies(compiledAssemblies);

  if (!ok)
  {
    DeleteTempFiles();
    return;
  }

  List<string> refs = new List<string>();
  List<string> sources = new List<string>();
  List<GraphicElement> rootSourceShapes = GetSources();
  rootSourceShapes.ForEach(root => GetReferencedAssemblies(root).Where(refassy => refassy is IAssemblyBox).ForEach(refassy => refs.Add(((IAssemblyBox)refassy).Filename)));

  // Get code for workflow boxes first, as this code will then be included in the rootSourceShape code listing.
  IEnumerable<GraphicElement> workflowShapes = canvasController.Elements.Where(el => el is IWorkflowBox);
  workflowShapes.ForEach(wf =>
  {
    string code = GetWorkflowCode(wf);
    wf.Json["Code"] = code;
  });

  // TODO: Better Linq!
  rootSourceShapes.Where(root => !String.IsNullOrEmpty(GetCode(root))).ForEach(root =>
  {
    CreateCodeFile(root, sources, GetCode(root));
  });

  exeFilename = String.IsNullOrEmpty(filename) ? "temp.exe" : Path.GetFileNameWithoutExtension(filename) + ".exe";
  Compile(exeFilename, sources, refs, true);
  DeleteTempFiles();
}

基本上,我们

  1. 先编译所有程序集
  2. 构建工作流代码并将其添加到源代码列表中
  3. 编译代码
  4. 在消息框中输出错误(真糟糕)

我发现的一个细微之处是,内存编译不会提供行号。也许我做错了什么。解决方法是创建临时文件并将它们映射到形状的 Text 属性,这样我就可以告诉你哪个形状的代码导致了错误。就像这样

protected void CreateCodeFile(GraphicElement root, List<string> sources, string code)
{
  string filename = Path.GetFileNameWithoutExtension(Path.GetTempFileName()) + ".cs";
  tempToTextBoxMap[filename] = root.Text;
  File.WriteAllText(filename, GetCode(root));
  sources.Add(filename);
}

我们通过查找实现 IAssemblyBox 的形状来确定哪些形状是程序集

protected bool CompileAssemblies(List<GraphicElement> compiledAssemblies)
{
  bool ok = true;

  foreach (GraphicElement elAssy in canvasController.Elements.Where(el => el is IAssemblyBox))
  {
    CompileAssembly(elAssy, compiledAssemblies);
  }

  return ok;
}

实际编译是一个递归过程,旨在确保所有引用的程序集都首先被编译

protected string CompileAssembly(GraphicElement elAssy, List<GraphicElement> compiledAssemblies)
{
  string assyFilename = ((IAssemblyBox)elAssy).Filename;

  if (!compiledAssemblies.Contains(elAssy))
  {
    // Add now, so we don't accidentally recurse infinitely.
    compiledAssemblies.Add(elAssy);

    List<GraphicElement> referencedAssemblies = GetReferencedAssemblies(elAssy);
    List<string> refs = new List<string>();

    // Recurse into referenced assemblies that need compiling first.
    foreach (GraphicElement el in referencedAssemblies)
    {
      string refAssy = CompileAssembly(el, compiledAssemblies);
      refs.Add(refAssy);
    }

    List<string> sources = GetSources(elAssy);
    Compile(assyFilename, sources, refs);
  }

  return assyFilename;
}

引用的程序集(给定一个我们知道是程序集的形状)是通过检查任何连接器来确定的,在这种情况下,是箭头的方向。不要创建方向错误的连接器,或者带有两个箭头的连接器,或者没有箭头的连接器。记住要念诵“原型!原型!原型!”的咒语。

protected List<GraphicElement> GetReferencedAssemblies(GraphicElement elAssy)
{
  List<GraphicElement> refs = new List<GraphicElement>();

  // TODO: Qualify EndConnectedShape as being IAssemblyBox
  elAssy.Connections.Where(c => ((Connector)c.ToElement).EndCap == AvailableLineCap.Arrow).ForEach(c =>
  {
    // Connector endpoint will reference ourselves, so exclude.
    if (((Connector)c.ToElement).EndConnectedShape != elAssy)
    {
      GraphicElement toAssy = ((Connector)c.ToElement).EndConnectedShape;
      refs.Add(toAssy);
    }
  });

  // TODO: Qualify EndConnectedShape as being IAssemblyBox
  elAssy.Connections.Where(c => ((Connector)c.ToElement).StartCap == AvailableLineCap.Arrow).ForEach(c =>
  {
    // Connector endpoint will reference ourselves, so exclude.
    if (((Connector)c.ToElement).StartConnectedShape != elAssy)
    {
      GraphicElement toAssy = ((Connector)c.ToElement).StartConnectedShape;
      refs.Add(toAssy);
    }
  });

  return refs;
}

顶级源(那些不是 IAssemblyBox 或 IFileBox 的)是一个有趣的技巧,它利用了这样一个事实:即使是分组的形状,在形状列表中也被表示为顶级元素。

protected bool ContainedIn<T>(GraphicElement child)
{
  return canvasController.Elements.Any(el => el is T && el.DisplayRectangle.Contains(child.DisplayRectangle));
}

protected List<GraphicElement> GetSources()
{
  List<GraphicElement> sourceList = new List<GraphicElement>();

  foreach (GraphicElement srcEl in canvasController.Elements.Where(
  srcEl => !ContainedIn<IAssemblyBox>(srcEl) &&
    !(srcEl is IFileBox)))
  {
    sourceList.Add(srcEl);
  }

  return sourceList;
}

“分组”其他代码形状的程序集形状(它们实际上不是分组框)是这样确定的,给定一个程序集框形状(或者实际上是任何其他形状,我也使用此方法来确定工作流“分组”中的代码)

protected List<string> GetSources(GraphicElement elAssy)
{
  List<string> sourceList = new List<string>();

  foreach (GraphicElement srcEl in canvasController.Elements.
    Where(srcEl => elAssy.DisplayRectangle.Contains(srcEl.DisplayRectangle)))
  {
    string filename = Path.GetFileNameWithoutExtension(Path.GetTempFileName()) + ".cs";
    tempToTextBoxMap[filename] = srcEl.Text;
    File.WriteAllText(filename, GetCode(srcEl));
    sourceList.Add(filename);
  }

  return sourceList;
}

而所有这些混乱的顶峰是源代码的编译(更多混乱,更多硬编码的东西,很有趣,很有趣!)。

protected bool Compile(string assyFilename, List<string> sources, List<string> refs, bool generateExecutable = false)
{
  bool ok = false;

  CSharpCodeProvider provider = new CSharpCodeProvider();
  CompilerParameters parameters = new CompilerParameters();

  parameters.IncludeDebugInformation = true;
  parameters.GenerateInMemory = false;
  parameters.GenerateExecutable = generateExecutable;

  parameters.ReferencedAssemblies.Add("System.dll");
  parameters.ReferencedAssemblies.Add("System.Data.dll");
  parameters.ReferencedAssemblies.Add("System.Data.Linq.dll");
  parameters.ReferencedAssemblies.Add("System.Core.dll");
  parameters.ReferencedAssemblies.Add("System.Net.dll");
  parameters.ReferencedAssemblies.Add("System.Net.Http.dll");
  parameters.ReferencedAssemblies.Add("System.Xml.dll");
  parameters.ReferencedAssemblies.Add("System.Xml.Linq.dll");
  parameters.ReferencedAssemblies.Add("Clifton.Core.dll");
  parameters.ReferencedAssemblies.AddRange(refs.ToArray());
  parameters.OutputAssembly = assyFilename;

  if (generateExecutable)
  {
    parameters.MainClass = "App.Program";
  }

  results = provider.CompileAssemblyFromFile(parameters, sources.ToArray());
  ok = !results.Errors.HasErrors;

  if (results.Errors.HasErrors)
  {
    StringBuilder sb = new StringBuilder();

    foreach (CompilerError error in results.Errors)
    {
      try
      {
        sb.AppendLine(String.Format("Error ({0} - {1}): {2}", 
         tempToTextBoxMap[Path.GetFileNameWithoutExtension(error.FileName) + ".cs"], 
         error.Line, 
         error.ErrorText));
      }
      catch
      {
        // other errors, like "process in use", do not have an associated filename, so general catch-all here.
        sb.AppendLine(error.ErrorText);
      }
    }

    MessageBox.Show(sb.ToString(), assyFilename, MessageBoxButtons.OK, MessageBoxIcon.Error);
  }

  return ok;
}

受够了吗?不,当然没有。让我们看看工作流代码是如何生成的!

工作流代码生成

另一个核心部分是这个。仍然很丑陋,仍然在 MenuController2.cs 中。

public string GetWorkflowCode(GraphicElement wf)
{
  StringBuilder sb = new StringBuilder();

  // TODO: Hardcoded for now for POC.
  sb.AppendLine("namespace App");
  sb.AppendLine("{");
  sb.AppendLine("\tpublic partial class " + wf.Text);
  sb.AppendLine("\t{");
  sb.AppendLine("\t\tpublic static void Execute(" + 
    Clifton.Core.ExtensionMethods.ExtensionMethods.LeftOf(wf.Text, "Workflow") + // Geez.
    " packet)");
  sb.AppendLine("\t\t{");
  sb.AppendLine("\t\t\t" + wf.Text + " workflow = new " + wf.Text + "();");

  // Fill in the workflow steps.
  GraphicElement el = FindStartOfWorkflow(wf);

  while (el != null)
  {
    sb.AppendLine("\t\t\tworkflow." + el.Text + "(packet);");
    el = NextElementInWorkflow(el);
  }

  sb.AppendLine("\t\t}");
  sb.AppendLine("\t}");
  sb.AppendLine("}");

  return sb.ToString();
}

我喜欢硬编码的命名空间和区分大小写的“Workflow”名称解析。

我们通过识别只有一个连接器起点连接到它的形状来找到工作流的起点(这是我在早期“哦,看那个”部分中略为提及的)。

protected GraphicElement FindStartOfWorkflow(GraphicElement wf)
{
  GraphicElement start = null;

  foreach (GraphicElement srcEl in canvasController.Elements.
    Where(srcEl => wf.DisplayRectangle.Contains(srcEl.DisplayRectangle)))
  {
    if (!srcEl.IsConnector && srcEl != wf)
    {
      // Special case for a 1 step workflow. Untested.
      if (srcEl.Connections.Count == 0)
      {
        start = srcEl;
        break;
      }

      // start and end has only one connection.
      if (srcEl.Connections.Count == 1 && ((Connection)srcEl.Connections[0]).ToConnectionPoint.Type == FlowSharpLib.GripType.Start)
      {
        start = srcEl;
        break;
      }
    }
  }

  return start;
}

然后工作流的下一步是通过找出连接器的终点连接到哪个形状来确定的。现在,老实说,我不知道这段代码有多健壮,我严重怀疑它不是非常健壮。

protected GraphicElement NextElementInWorkflow(GraphicElement el)
{
  GraphicElement ret = null;

  if (el.Connections.Count == 1)
  {
    if (((Connector)((Connection)el.Connections[0]).ToElement).EndConnectedShape != el)
    {
      ret = ((Connector)((Connection)el.Connections[0]).ToElement).EndConnectedShape;
    }
  }
  else if (el.Connections.Count == 2)
  {
    if (((Connector)((Connection)el.Connections[0]).ToElement).StartConnectedShape == el)
    {
      ret = ((Connector)((Connection)el.Connections[0]).ToElement).EndConnectedShape;
    }
    else if (((Connector)((Connection)el.Connections[1]).ToElement).StartConnectedShape == el)
    {
      ret = ((Connector)((Connection)el.Connections[1]).ToElement).EndConnectedShape;
    }
  }

  return ret;
}

好的,希望现在你受够了!

想看更疯狂的东西吗?

这是托管语义 Web 服务器的一个示例。

(更大的图片)

尝试使用 Article\st_text.fsd。运行“应用程序”,然后在浏览器中,给它一些东西输出到控制台窗口,例如 URL localhost:8001/ST_Text?text=Hello+World

这有点像自己动手,因为你看,上面的“应用程序”正在使用模块管理器、服务管理器、语义发布/订阅等来运行时加载向自身注册为语义订阅者的模块,而 Web 服务器解析路由以实例化语义类型,然后发布该类型,订阅者将文本输出到控制台窗口。(这只是一个演示所有你可以做到的很酷的事情!)作为一个概念证明,这表明 FlowSharpCode 可以用来实现它本身,并且将来很可能会这样做。

作为给读者的练习,Article 文件夹中有一个“speak receptor.fsd”组件。创建一个名为 ST_Speak 的语义类型(使用 ST_Text 作为模板),导入 speak receptor 组件,为“st_speak”添加路由,并使用 URL localhost:8001/ST_Speak?text=Hello+World 运行应用程序。你必须将 ST_Speak 类和 speak receptor 放在正确的程序集中(提示:取消分组程序集以放置形状,以便它们包含在正确的程序集形状中。)

那么这里真正酷的东西是什么?

这是我的简短列表

  • 创建程序集并引用它非常非常简单
  • 我发现将一组形状(及其代码)复制到另一个绘图上非常有趣
  • 我喜欢数据包工作流的方法——它实际上使代码非常简单、模块化且线程安全。方法参数和返回值都可以,但数据包化工作流使添加或删除工作流步骤变得非常统一和简单。可能不适合所有人。
  • 以这种方式工作实际上非常有趣。智能感知有点受限,但我发现我并不想念它。IDE 可以改进很多,但目前来说它是可用的。
  • 我可以想象,如果你尝试将这种方法引入主流开发公司(当然,使用比这个代码更高级的版本),将会引起一片哗然。而我从想象这些哗然声中获得一种虐待狂的乐趣。
  • 将代码语言、平台、后端、前端等混合在一个表面上是一个非常有趣的想法。我最初曾想做一个 C# 客户端调用 Django/Python 后端的 REST 的演示,并可能在某个时候实现它。

我接下来会做什么?

  1. 清理代码
  2. 添加多文档支持
  3. 提高可用性——我的首要任务之一是将图的一部分折叠到页外引用,这样这些图就可以管理了。

我希望你做什么?

玩耍!

玩玩示例,自己尝试一些简单的事情。请记住代码对你施加的硬编码限制,例如命名空间、Program 类名等。我发现 FlowSharp 有一些我将要解决的怪癖,但最重要的是,我对你对这种疯狂方法的看法、你希望看到什么,以及你是否认为这个概念值得进一步追求(我认为是,所以我想你的想法无关紧要,嘿嘿!)如果有任何反馈,请尽可能使用 GitHub Issues,否则请在此评论部分发布你的反馈,我将尽力将其添加到项目的 Issues 中。

© . All rights reserved.