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

展示视觉计算概念的数据中心代理 APOD 幻灯片

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2018年5月4日

CPOL

25分钟阅读

viewsIcon

9760

downloadIcon

99

C#、C# 与 ClearScript + Javascript 以及纯 Javascript 示例

目录

引言

在过去的几年里,我通过各种文章一直在撰写和思考各种想法——视觉编程、数据中心计算、语义计算等等。这些年来,一直有一个令人不安的想法,即命令式(甚至函数式)编程是倒退的——数据应该驱动工作流,而不是工作流推动数据,如果我们从数据驱动的方法入手,就可以实际编写非常小的可重用组件并以可视化方式将它们连接起来。

虽然我们都从这个开始,并且仍然必须在某种程度上处理它

请,不要这样(来自 MagPi Magazine 对 Scratch 的评论

但更像这样(截图来自 Node-RED 工具)

我实际上想要介于 Scratch 和 Node-RED 之间的一种东西。Scratch 是对赋值、if-then-else 和循环等编程原语的一种糟糕的视觉表示。Node-RED 很好,但对于我正在尝试做的事情有两个问题。首先,它太高层了——在我的世界中,Node-RED 中展示的高层组件应该由设计工具中创建的低层构造组成。其次,Node-RED 的实现似乎是以工作流为中心而不是以数据为中心——这里的区别在于线条将输出连接到输入,而在我所做的数据中心编程工作中,线条是代理理解放置在数据池中的数据类型的产物。

我使用 DRAKON 编程语言的工作(图片剪辑自 维基百科 上的示例)

再次,它太低层且以工作流为中心而不是以数据为中心。

建议的解决方案

我提出的解决方案借鉴了函数式编程并添加了一些东西。

运算符

首先,大多数代码都在执行映射、归约或过滤操作。数据要么是

  • 正在映射:从一种表示形式转换为另一种表示形式。
  • 正在归约:聚合、累积或以某种方式处理为单个值。
  • 正在过滤:我们不需要的数据正在被移除。

但这还不够——我们至少需要“匹配和映射”以及循环的能力

  • 匹配和映射:当输入数据匹配特定条件时,它被映射到另一种表示/类型
  • 循环:任何一组事物都会自动迭代。此外,可以使用映射和“匹配和映射”原语创建基本循环。

数据感知代理

拼图的另一个重要部分借鉴了我以前的工作——由数据“发布”触发的代理。关键的认识是上面描述的操作符(映射、归约、过滤、匹配)实际上是代理!

一个简单示例 - 计数器

这是一个简单的“从 1 数到 10”的例子。

在这个例子中,我们有三个代理

  1. 一个将数据包内容记录到控制台的代理。
  2. 一个通用的匹配代理,测试数字是否小于 10 的条件,如果小于,则将数据重新映射到新类型。
  3. 一个通用的映射代理,将数据从一种形式“转换”为另一种形式,在此示例中只是递增数字。

从视觉上看,这就是程序的构建方式(即使你讨厌这些图标)。因为程序是以数据为中心的,所以数据类型(用绿色表示)必须改变才能触发处理该类型的代理。在这个简单的例子中,这看起来有点荒谬,但在实际例子中它更有意义,我们稍后会看到。

因为我还没有完成设计器的实现(那是下一篇文章),无论使用 FlowSharp 还是 FlowSharpWeb,我们目前都不得不查看实际的 C# 代码来实例化代理和种子数据。

private static void SimpleCounterExample()
{
  var seed = CreateExpando(new Dictionary<string, dynamic>() 
  { 
    { "Context", "Counter" }, 
    { "Type", "Number" }, 
    { "val", 1 } 
  });

  var logAgent = new ConsoleLogAgent("Counter", "Number", "NextNumber");

  var matchAgent = new MatchAgent("Counter", "NextNumber", "IncrementNumber").
    Add((context, data) => data.val < 10, (context, data) => data);

  var incrementAgent = new MapAgent("Counter", "IncrementNumber", "Number") 
  { 
    Map = (context, data) => CreateExpando(new Dictionary<string, dynamic>() { { "val", data.val + 1 } })
  };

  agentPool.Add(logAgent);
  agentPool.Add(matchAgent);
  agentPool.Add(incrementAgent);

  dataPool.Enqueue(seed);
}

为了得到这个,需要大量的代码行

幕后隐藏着代理正在做的事情。此时,我假设你认为我疯了!但请耐心听我说,这会变得更有趣。上面计数器示例的重点是,你永远不会实际编写那些 C# 代码——你会像我上面那样绘制计数器。此外,所有这些 ExpandoObject 业务都是 C# 的一个烦恼,作为一个强类型语言,在处理编译时不确定的类型时——请记住,“程序”是在运行时编写的。在 Python 或 Javascript 中编写这些代码实际上要容易得多!

还值得注意的是,本文讨论 ClearSharp 和 Javascript 的原因是因为映射函数是脚本化的,并且对于匹配代理,限定符也是脚本化的。这使我们能够创建通用的中级计算代理。凭借最少的编程技能(每个人都使用 Excel,对吧?),用户可以创建简单的条件和映射。真正困难的工作是了解你正在处理的数据!

关于...

NASA 的 API 密钥

请将 C# 和 Javascript 中的“[你的 API 密钥]”替换为从 https://api.nasa.gov/index.html#apply-for-an-api-key 获取的 API 密钥

源代码中有三个地方需要这样做

在 apodPureCSharp 项目的 Program.cs 中,第 174 行

ApiKey = "[your API key]",

在 apod 项目的 Program.cs 中,第 188 行

ApiKey = "[your API key]",

在 apodSlideShow.html 中,第 192 行

ApiKey: "[your API key]"

ClearScript DLL

下载包已经包含 bin\Debug 文件夹中编译好的 ClearScript DLL,这就是为什么下载包有 9MB 的原因!这省去了你自行编译 ClearScript 的步骤。附带一提,不要在虚拟机中运行此项目 - 当我尝试 ClearScript 版本的项目(在解决方案中称为“apod”)时,它在 Oracle VirtualBox 虚拟机中不起作用。

项目

  • apod - 这是 C# + ClearScript 与 Javascript 脚本。
  • apodPureCSharp - 这是纯 C# 版本,使用 Lambda 表达式。
  • apodSlideShow.html - 这是纯 Javascript 版本,在 apod 项目中。不要在浏览器中打开文件——你需要一个服务器,因为有跨域请求。

上下文和类型

我之前发表了一篇关于上下文数据浏览器的文章,因为其核心观点是脱离上下文的数据是毫无意义的。在这篇文章中,虽然存在上下文的概念,但它实际上只是处理更复杂应用程序的占位符。在此演示的“上下文”中,我们只处理 APOD“上下文”的数据类型,因此无需在不同上下文中探索代理。为数据拥有不同的类型就足够了。

动态和 ExpandoObject

我在这里通过使用 dynamicExpandoObject 违反了很多规则——至少在 C# 中,通常会创建强类型类并对数据进行序列化和反序列化。这是完全可行的,但完全违背了拥有一个对它操作的数据一无所知的低级代理的目的。规则——限定符和映射函数——而是作为脚本编码的,我不想在运行时编译 C# 代码。ExpandoObjectdynamic 类型是管理无类型数据的绝佳方式。结果是代码的行为更像 Javascript 和 JSON 中无类型的键值字典,事实证明这正是我们想要的。

代理符号

视觉上,我将对各种代理使用以下“正式”符号。

映射代理

映射代理

  1. 接受指定的输入类型。
  2. 执行转换数据的映射脚本。
  3. 发布带有指定输出类型的映射数据。

请注意,映射函数本身可以指定上下文和输出类型,允许转换覆盖代理构建时指定的输出类型。

匹配代理

匹配代理,按照指定匹配限定符-映射的顺序

  1. 接受指定的输入类型。
  2. 执行限定符脚本。
  3. 如果限定符脚本返回 true
    1. 执行关联的映射脚本。
    2. 除非被映射脚本覆盖,否则生成的映射数据将与输出类型一起发布。
    3. 当找到第一个返回 true 的限定符时,匹配处理停止。
  4. 如果限定符脚本返回 false,则测试下一个匹配。
  5. 如果没有找到匹配项,则不会发布任何数据,从而终止该数据流分支的数据流。

输出代理

输出代理

  1. 接受指定的输入类型。
  2. 执行构造函数中指定的函数,该函数提供将数据输出到所需“设备”的接口。
  3. 发布带有指定输出类型的输入数据。

自定义代理

自定义代理执行来自现有库的代理或用户创建的自定义代理。它

  1. 接受指定的输入类型。
  2. 执行一些操作。
  3. 以指定的输出类型发出操作结果。

此演示中使用的自定义代理示例包括

  • HttpGetJsonAgent
  • HttpGetImageAgent
  • SleepAgent

一个更复杂的示例 - 每日天文图片 (APOD) 幻灯片

现在是深入探讨如何将代理数据中心系统组合在一起的时候了。我将通过几种不同的方式演示这个概念

  1. 纯 C# - 没有脚本,特定于应用程序的“代码”以 C# 匿名 lambda 表达式的形式实现。
  2. C# 代理和 Javascript 的混合,使用 ClearScript 解释,用于特定于应用程序的条件和映射。
  3. 一个可以在浏览器中运行的纯 Javascript 实现。

APOD 响应是什么样的?

以下是我们正在使用的示例

{
  "date": "2018-01-02", 
  "explanation": "Why does the Perseus galaxy cluster shine so strangely in one specific color of X-rays? [etc...]
  "hdurl": "https://apod.nasa.gov/apod/image/1801/PerseusCluster_DSSChandra_3600.jpg", 
  "media_type": "image", 
  "service_version": "v1", 
  "title": "Unexpected X-Rays from Perseus Galaxy Cluster", 
  "url": "https://apod.nasa.gov/apod/image/1801/PerseusCluster_DSSChandra_960.jpg"
}

特别值得注意的是媒体类型,以及许多图像中同时存在高清和“低清”图像。

代理

接下来定义 APOD 幻灯片所需的代理及其脚本。有几点需要注意

  1. 在此示例中,我使用 C# lambda 表达式而不是 Javascript 作为限定符脚本。在这些示例中,Javascript 和 C# 之间几乎没有区别。
  2. 映射脚本,虽然在“仅 C#”示例中作为 C# 匿名函数实现,但实际上被描述为 JSON 对象,因为这将是混合 C# - ClearScript 和纯 Javascript 示例的共同之处。
  3. 由于算法是以数据为中心的,因此没有工作流,也没有将代理粘合在一起的箭头。您能根据输入/输出数据类型找出工作流吗?我提供了第二个图表,说明了通过输入/输出类型粘合在一起的代理,这让您对仅由指定输入/输出类型产生的工作流有了一个概念。

将输入数据字段和上下文字段转换为适合请求 APOD 信息的单个 URL,该信息发布在数据池中。

执行 HTTP GET 并发布结果 JSON。

 

将“media_type”JSON 字段与“image”匹配。如果匹配,则将数据发布为“ApodNotVideo”,否则将数据发布为“DateCheck”,这会启动获取下一个日期的过程。

 

自定义类型会覆盖默认输出类型以触发日期检查处理程序。此映射为:{ Type : "DateCheck", date : data.date }

输出日期、标题和解释。请注意,此代理不发布任何输出类型,因此数据流执行在此数据流分支上停止。

 

如果存在图像的高清版本,则使用该图像,否则使用 JSON“url”字段中的 URL。

从提供的 URL 值检索图像。

在图片框中显示图像。

在图像上暂停指定的秒数。

检查是否已达到当前日期。如果未达到,则将当前数据包发布为“Date”类型。如果匹配语句失败,则由于未发布任何内容,数据流过程将终止。

将当前日期映射到下一个日期并发布为“NextDate”类型。

将当前日期映射到请求下一个 APOD 图像所需的数据包中,并将数据包发布到数据池。

 

这是同样的东西,但我根据输入/输出类型连接了数据流

仅 C# 实现

我们将首先查看仅用 C# 编写的实现。

代理

代理非常简单。这就是所有这一切的重点——编写小的代码块,将它们连接起来做更大的事情。

代理基类

在纯 C# 实现中,这里并没有太多发生。

public abstract class Agent
{
  public string Context { get; protected set; }
  public string DataType { get; protected set; }
  public string ResponseContext { get; set; }
  public string ResponseDataType { get; set; }
  public dynamic ContextData { get; set; }

  public Agent(string context, string dataType, string responseDataType)
  {
    Context = context;
    DataType = dataType;
    ResponseDataType = responseDataType;
  }

  public abstract void Call(dynamic data);

  public void SetContextAndType(dynamic data, dynamic resp, bool useAgentContextAndType)
  {
    if (useAgentContextAndType || !((IDictionary<string, object>)resp).ContainsKey("Context"))
    {
      resp.Context = ResponseContext ?? Context;
    }
  
    if (useAgentContextAndType || !((IDictionary<string, object>)resp).ContainsKey("Type"))
    {
      resp.Type = ResponseDataType;
    }
  }
}

这里正在进行的重点是处理当映射数据指定上下文和/或类型时的覆盖。由于这两个元素是可选的,即使 useAgentContextAndType 为 false,我们仍然检查它们是否存在,如果这些元素未在映射函数中定义,则使用代理的上下文和输出类型。

映射代理

public class MapAgent : Agent
{
  public Func<dynamic, dynamic, dynamic> Map { get; set; }

  protected bool useAgentContextAndType;

  public MapAgent(string context, string dataType, string responseDataType, bool useAgentContextAndType = true) : base(context, dataType, responseDataType)
  {
    this.useAgentContextAndType = useAgentContextAndType;
  }

  public override void Call(dynamic data)
  {
    var resp = Map == null ? ((Func<dynamic, dynamic, bool>)data.Map)(ContextData, data) : Map(ContextData, data);
    SetContextAndType(data, resp, useAgentContextAndType);
    Program.QueueData(resp);
  }
}

三行代码处理

  1. 接收数据。
  2. 使用代理实例化时指定的映射函数,或数据包本身中指定的映射函数。
  3. 发布映射数据。

HTTP Get JSON 代理

public class HttpGetJsonAgent : Agent
{
  public HttpGetJsonAgent(string context, string dataType, string responseDataType) : base(context, dataType, responseDataType)
  {
  }

  public async override void Call(dynamic data)
  {
    HttpClient client = new HttpClient();
    using (var response = await client.GetAsync(data.url, HttpCompletionOption.ResponseHeadersRead))
    {
      if (response.IsSuccessStatusCode)
      {
        using (var stream = await response.Content.ReadAsStreamAsync())
        {
          using (var streamReader = new StreamReader(stream))
          {
            var str = await streamReader.ReadToEndAsync();
            dynamic resp = JsonConvert.DeserializeObject<ExpandoObject>(str);
            resp.Context = ResponseContext ?? data.Context;
            resp.Type = ResponseDataType;
            Program.QueueData(resp);
          }
        }
      }
    }
  }
}

这里的大部分工作都在设置以获取响应。一旦收到 JSON 响应,它就会被反序列化为一个 ExpandoObject 并发布到数据池中。

HTTP Get 图像代理

public class HttpGetImageAgent : Agent
{
  public HttpGetImageAgent(string context, string dataType, string responseDataType) : base(context, dataType, responseDataType)
  {
  }

  public async override void Call(dynamic data)
  {
    HttpClient client = new HttpClient();

    using (var response = await client.GetAsync(data.url, HttpCompletionOption.ResponseHeadersRead))
    {
      if (response.IsSuccessStatusCode)
      {
        using (var stream = await response.Content.ReadAsStreamAsync())
        {
          var image = Image.FromStream(stream);
          data.Image = image;
          data.Context = ResponseContext ?? data.Context;
          data.Type = ResponseDataType;
          Program.QueueData(data);
        }
      }
    }
  }
}

同样,接收图像利用 Image 类处理输入流。

输出代理

public class OutputAgent : Agent
{
  protected Action<dynamic> action;

  public OutputAgent(string context, string dataType, string responseDataType, Action<dynamic> action) : base(context, dataType, responseDataType)
  {
    this.action = action;
  }

  public override void Call(dynamic data)
  {
    action(data);
    data.Context = ResponseContext ?? data.Context;
    data.Type = ResponseDataType;
    Program.QueueData(data);
  }
}

这里的突出点是,该代理执行一个操作,传入数据,然后将代理发布回数据池。显然(或者至少应该显然),发布的类型应该为 null 或与输入类型不同的类型,否则将发生无限循环。

休眠代理

public class SleepAgent : Agent
{
  protected int msSleep;

  public SleepAgent(string context, string dataType, string responseDataType, int msSleep) : base(context, dataType, responseDataType)
  {
    this.msSleep = msSleep;
  }

  public override void Call(dynamic data)
  {
    Thread.Sleep(msSleep);
    data.Context = ResponseContext ?? data.Context;
    data.Type = ResponseDataType;
    Program.QueueData(data);
  }
}

在这里,此代理只是暂停数据流的处理指定毫秒数,然后将数据以指定的“输出”类型发布到数据池。

匹配代理

public class MatchAgent : Agent
{
  protected List<(Func<dynamic, dynamic, bool> condition, Func<dynamic, dynamic, dynamic> map, bool useAgentContextAndType)> matches = 
  new List<(Func<dynamic, dynamic, bool>, Func<dynamic, dynamic, dynamic>, bool)>();

  public MatchAgent(string context, string dataType, string responseDataType) : base(context, dataType, responseDataType)
  {
  }

  public MatchAgent Add(Func<dynamic, dynamic, bool> condition, Func<dynamic, dynamic, dynamic> map, bool useAgentContextAndType = true)
  {
    matches.Add((condition, map, useAgentContextAndType));

    return this;
  }

  public override void Call(dynamic data)
  {
    var match = matches.FirstOrDefault(t => t.condition(ContextData, data));

    if (!match.Equals((null, null, false)))
    {
      dynamic resp = match.map(ContextData, data);
      SetContextAndType(data, resp, match.useAgentContextAndType);
      Program.QueueData(resp);
    }
  }
}

此类别实现了通用匹配代理。如果条件返回 true,则执行关联的映射函数,并根据代理实例化时指定的输出类型或结果映射数据中指定的上下文和类型发布数据。

代理实例化

以下显示了实例化每个代理的代码。

映射 URL、API 密钥和日期

var apodUrlMappingAgent = new MapAgent(Contexts.APOD, "ApodRequestData", "ApodUrl")
{
  ContextData = contextData,
  Map = (context, data) => CreateExpando(new Dictionary<string, dynamic>() 
  { 
    { "url", context.Url + "?api_key=" + context.ApiKey + "&date=" + data.date }
  })
};

从 APOD Web 服务获取 JSON

var apodAgent = new HttpGetJsonAgent(Contexts.APOD, "ApodUrl", "ApodResponseData");

匹配媒体类型

var apodMediaTypeFilter = new MatchAgent(Contexts.APOD, "ApodResponseData", "ApodNotVideo").
  Add((context, data) => data.media_type == "image", (context, data) => data).
  Add((_, __) => true, (context, data) => 
    CreateExpando(new Dictionary<string, dynamic>() 
    { 
      { "Type", "DateCheck" }, { "date", data.date } 
    }), false);

这里注意两件事

  1. 首先,请记住,匹配是按照它们添加的顺序进行测试的。如果媒体类型不是“image”,则处理第二个匹配,因为它总是返回 true
  2. 我们用 false 表示我们正在覆盖输出数据类型,该类型在映射中指定。

输出日期、标题和解释

var apodTextToControlAgent = new OutputAgent(Contexts.APOD, "ApodNotVideo", null, data =>
{
  form.SetDate(data.date);
  form.SetTitle(data.title);
  form.SetExplanation(data.explanation);
});

在此代理中,我们传入更新日期、标题和解释文本框的动作。因为这可能异步发生,所以我们将实际实现包装在 Invoke 中,例如

this.Invoke(() => pbApod.Image = image);

因为我在各种 WinForm 实现中经常这样做,所以它被实现为一个扩展方法(如果我们要将动作排队到窗口消息循环处理中,我们可以使用 BeginInvoke

public static void Invoke(this Control control, Action action)
{
  if (control.InvokeRequired)
  {
    // We want a synchronous call here!!!!
    control.Invoke((Delegate)action);
  }
  else
  {
    action();
  }
}

如果可能,获取高清图像

var apodBestImageAgent = new MatchAgent(Contexts.APOD, "ApodNotVideo", "ImageUrl").
  Add((context, data) => 
    !String.IsNullOrEmpty(data.hdurl), 
    (context, data) => 
      CreateExpando(new Dictionary<string, dynamic>() 
      { 
        { "url", data.hdurl }, { "date", data.date } 
      }
    )).
    Add((_, __) => true, 
      (context, data) => 
        CreateExpando(new Dictionary<string, dynamic>() 
        { 
          { "url", data.url }, { "date", data.date } 
        }
    )
  );

再次注意,匹配是按照它们添加的顺序进行测试的,因此如果高清 URL 不存在,则“默认”使用 URL。

获取图像

var apodImageAgent = new HttpGetImageAgent(Contexts.APOD, "ImageUrl", "ApodImage");

显示图像

var imageToPictureBoxAgent = new OutputAgent(Contexts.APOD, "ApodImage", "ApodDelay", 
  data => form.SetImage(data.Image));

暂停指定时间

var sleepAgent = new SleepAgent(Contexts.APOD, "ApodDelay", "DateCheck", 10000);

还有更多日期要处理吗?

var dateCheckAgent = new MatchAgent(Contexts.APOD, "DateCheck", "Date").
  Add((context, data) => 
    DateTime.Parse(data.date + " 23:59:59") < DateTime.Now, (context, data) => data);

请注意,此匹配没有“默认”情况——如果没有更多日期要处理,则数据流执行终止。

获取下一个日期

var apodNextDateAgent = new MapAgent(Contexts.APOD, "Date", "NextDate")
{
  Map = (context, data) => 
    CreateExpando(new Dictionary<string, dynamic>() 
    { 
      { "date", DateTime.Parse(data.date).AddDays(1).ToString("yyyy-MM-dd") } 
    }
  )
};

为下一个 APOD 组织请求

var apodNextImageAgent = new MapAgent(Contexts.APOD, "NextDate", "ApodRequestData", false)
{
  ContextData = contextData,
  Map = (context, data) => 
    CreateExpando(new Dictionary<string, dynamic>() 
    { 
      { "Url", context.Url }, { "ApiKey", context.ApiKey }, { "date", data.date }
    }
  )
};

此时,处理从我们开始的地方继续。

注册代理

代理创建后,它们在代理池中注册

agentPool.Add(apodUrlMappingAgent);
agentPool.Add(apodAgent);
agentPool.Add(apodMediaTypeFilter);
agentPool.Add(apodBestImageAgent);
agentPool.Add(apodImageAgent);
agentPool.Add(apodTextToControlAgent);
agentPool.Add(imageToPictureBoxAgent);
agentPool.Add(apodNextDateAgent);
agentPool.Add(apodNextImageAgent);
agentPool.Add(sleepAgent);
agentPool.Add(dateCheckAgent);

应用程序播种

应用程序使用我们想要开始幻灯片播放的日期进行播种,我随意将其设置为年初(2018年)

private static void RegisterInitialDataLoad()
{
  object data = new
  {
    Context = Contexts.APOD,
    Type = "ApodRequestData",
    date = "2018-01-01",
  };

QueueData(data);
}

上下文数据

上下文数据包含“不变”数据,即基本 URL 和您的 API 密钥

private static object CreateContextData()
{
  object data = new
  {
    Url = "<a href="https://api.nasa.gov/planetary/apod">https://api.nasa.gov/planetary/apod</a>",
    ApiKey = "[your API key]",
  };

  return data;
}

创建 ExpandoObject

我决定为此编写一个辅助函数,即使它是一个字典键值对,也能让映射操作看起来更像 JSON

private static dynamic CreateExpando(Dictionary<string, dynamic> collection)
{
  var obj = (IDictionary<string, object>)new ExpandoObject();
  collection.ForEach(kvp => obj[kvp.Key] = kvp.Value);

  return obj;
}

处理循环

用于处理数据池中数据(同步或异步)的循环

private static void StartProcessing(Action processor)
{
  Task.Run(() =>
  {
    while (true)
    {
      dataSemaphore.WaitOne();
      processor();
    }
  });
}

同步和异步处理器分别在 ProcessSynchronouslyProcessAsynchronously 方法中实现。

数据通过以下方式放置到数据池中

public static void QueueData(dynamic data)
{
  dataPool.Enqueue(data);
  dataSemaphore.Release();
}

取决于您是要执行同步处理还是异步处理

private static void ProcessSynchronously()
{
  dataPool.TryDequeue(out dynamic data);
  var agents = agentPool.Where(a => a.Context == data.Context && a.DataType == data.Type).ToList();
  Log(agents, data);
  agents.ForEach(agent => agent.Call(data));
}

private static void ProcessAsynchronously()
{
  dataPool.TryDequeue(out dynamic data);
  var agents = agentPool.Where(a => a.Context == data.Context && a.DataType == data.Type).ToList();
  Log(agents, data);
  agents.ForEach(agent => { Task.Run(() => agent.Call((object)data)); });
}

调试时,同步处理更容易操作。

记录器输出代理名称、其上下文以及它正在处理的数据类型,我们可以在控制台窗口中查看,该窗口是在我们将应用程序的输出类型设置为控制台窗口时创建的,尽管它是一个 WinForm 应用程序

日志看起来像这样

C# 和 ClearScript 实现

为了避免对代码中应用程序特定部分的运行时编译(即条件和映射函数),我研究了使用 ClearScript,以便我可以用 Javascript 编写条件和映射函数。这使我们更接近于能够使用以本地语言实现的预制代理,以可视化方式构建应用程序的目标。只需要编写脚本代码,即所谓的“低代码”。根据 维基百科:“低代码开发平台 (LCDP) 允许通过图形用户界面和配置来创建应用程序软件,而不是传统的程序化计算机编程。”

使用 ClearScript

ClearScript 的代码库构建没有任何问题——请完全按照 ClearScript 文档中描述的说明进行操作。ClearScript 构建完成后,您需要做两件事

1. 添加对 ClearScript.dll 的引用

2. 将其他 DLL 复制到 bin\Debug 或 bin\Release 文件夹

托管 C# 对象

使用 ClearScript 可以做的一件事是托管您的 C# 对象,然后可以直接在 Javascript 中引用它们。在上面的纯 C# 示例中,不变数据有一个“上下文”对象,变体数据有一个“数据”对象。使用 ClearScript,我们可以托管多个上下文对象以及数据对象,这在我们要从不同的“小程序”提供多个不变上下文时很有用。这很容易通过字符串-对象字典实现,其中上下文被注册,然后托管

public void InitializeContextData()
{
  ContextData?.ForEach(kvp => engine.AddHostObject(kvp.Key, kvp.Value));
}

托管 C# 对象的细微差别

但故事并未就此结束。当您评估 Javascript 表达式时,返回类型是 V8ScriptItem。结合动态语言运行时 (DLR),您可以使用 [object].[field] 符号轻松访问返回表达式的成员,但是,您不能仅仅将 V8ScriptItem 传递给另一个 Javascript 函数——会发生一些糟糕的事情,包括堆栈溢出异常。我们必须通过这个圈套

public void HostData(dynamic data)
{
  // If it's a V8ScriptItem, then the Keys must be defined!
  if (data.GetType().Name == "V8ScriptItem" && Keys == null)
  {
    throw new Exception("Keys must be defined for the host data in this context.datatype: " + Context+"."+DataType);
  }

  // Kludge because the data returned from a map of a match is a V8ScriptItem which cannot be added as a host object!
  if (data.GetType().Name != "V8ScriptItem")
  {
    engine.AddHostObject("data", data);
  }
  else
  {
    var data2 = new ExpandoObject() as IDictionary<string, object>;
    Keys.ForEach(key => data2[key] = data[key]);
    engine.AddHostObject("data", data2);
  }
}

上面的代码将很容易地托管一个非 V8ScriptItem,将主机对象命名为“data”。对于 V8ScriptItem,我们必须将其转换为 ExpandoObject。因为我们不知道 V8ScriptItem 对象中的 Javascript 成员,所以我们需要提供一个模式(这里称为“Keys”)来让我们将数据从 V8ScriptItem 移动到 ExpandoObject

调用脚本

与 ClearScript 配合使用的映射代理如下所示

public override void Call(dynamic data)
{
  InitializeContextData();
  HostData(data);
  var resp = Map == null ? engine.Evaluate(data.Map) : engine.Evaluate(Map);
  SetContextAndType(data, resp, useAgentContextAndType);
  Program.QueueData(resp);
}

将其与前面提到的纯 C# 示例进行对比

public override void Call(dynamic data)
{
  var resp = Map == null ? ((Func<dynamic, dynamic, bool>)data.Map)(ContextData, data) : Map(ContextData, data);
  SetContextAndType(data, resp, useAgentContextAndType);
  Program.QueueData(resp);
}

基本上是同样的东西,只是添加了上下文和数据托管。

匹配代理类似,但请注意关于使用 vardynamic 的代码注释

public override void Call(dynamic data)
{
  InitializeContextData();
  HostData(data);
  var match = matches.FirstOrDefault(t => (bool)engine.Evaluate(t.condition) == true);

  if (!match.Equals((null, null, false)))
  {
    // Can't use var here! Why not, I used var in MapAgent!
    dynamic resp = engine.Evaluate(match.map);

    // and if the Context and Type has been deleted from a map operation, nothing can coerce the Context back in to the ExpandoObject.
    // For example, this doesn't work - an exception is thrown that the ExpandoObject doesn't have Context.
    // ((dynamic)resp).Context = ResponseContext ?? data.Context;
    // Casting to an IDictionary doesn't work either!

    SetContextAndType(data, resp, match.useAgentContextAndType);
    Program.QueueData(resp);
  }
}

这些就是我在使用 ClearScript 时发现的细微差别!

使用 Javascript 脚本实例化代理

所有其他代理都遵循上述相同的模式,并且与纯 C# 实现非常相似,但有一个细微差别——即使数据传入类型被定义为动态,我们实际上也必须将类型强制转换为动态才能访问成员。例如,在 HttpGetImageAgent

我之前说过,您可以使用 [object].[field] 符号访问成员字段——这在调用 execute 或 evaluate 方法的代码中是正确的。一旦对象通过 lambda 表达式或在 Task.Run 块中传递,转换为 dynamic 突然成为一个要求。我怀疑这与 DLR 有关,但我真的不知道发生了什么。

那么,让我们看看代理是如何实例化的,这次使用 Javascript 指定条件和映射。将其与前面介绍的纯 C# 代码进行对比。您会注意到的最重要的事情是,不再需要从字符串-对象字典中 CreateExpando,但我们根据需要添加了指定返回对象模式。另请注意,我只展示了使用 Javascript 的代理实例化——其他代理实例化与上面纯 C# 代码中介绍的相同。

映射 URL、API 密钥和日期

var apodUrlMappingAgent = new MapAgent(Contexts.APOD, "ApodRequestData", "ApodUrl")
{
  Keys = new List<string> { "Type", "Url", "ApiKey", "date" },
  ContextData = new Dictionary<string, object>() { { "context", contextData } },
  Map = "({url: context.Url + '?api_key=' + context.ApiKey + '&date=' + data.date})",
};

匹配媒体类型

var apodMediaTypeFilter = new MatchAgent(Contexts.APOD, "ApodResponseData", "ApodNotVideo").
  Add("data.media_type == 'image'", "data").
  Add("true", @"({Type: 'Date', date: data.date})", false);

这里注意两件事

  1. 首先,请记住,匹配是按照它们添加的顺序进行测试的。如果媒体类型不是“image”,则处理第二个匹配,因为它总是返回 true
  2. 我们用 false 表示我们正在覆盖输出数据类型,该类型在映射中指定。

如果可能,获取高清图像

var apodBestImageAgent = new MatchAgent(Contexts.APOD, "ApodNotVideo", "ImageUrl") { Keys = new List<string> { "url", "hdurl" } }.
  Add("data.hdurl !== undefined && data.hdurl != ''", "({url: data.hdurl, date: data.date})").
  Add("true", "({url: data.url, date: data.date})");

还有更多日期要处理吗?

var dateCheckAgent = new MatchAgent(Contexts.APOD, "DateCheck", "Date") { Keys = new List<string> { "date" } }.
  Add("new Date(data.date + ' 23:59') < Date.now()", "data");

获取下一个日期

Javascript 中的日期处理和格式化非常糟糕,因为我们必须为小于 10 的数字前置“0”,并处理 UTC 时间与本地时间中的日期。

var apodNextDateAgent = new MapAgent(Contexts.APOD, "Date", "NextDate")
{
  Keys = new List<string> { "date" },
  Map = @"
var nextDate = new Date(data.date + ' 0:01'); 
nextDate.setDate(nextDate.getDate() + 1);
({ date: nextDate.getFullYear()+'-'+('0' + (nextDate.getMonth()+1)).slice(-2) + '-' + ('0' + nextDate.getDate()).slice(-2)})"
};

为下一个 APOD 组织请求

var apodNextImageAgent = new MapAgent(Contexts.APOD, "NextDate", "ApodRequestData", false)
  {
    Keys = new List<string> { "date" },
    ContextData = new Dictionary<string, object>() { { "context", contextData } },
    Map = "({date: data.date,})"
};

关于 C# - ClearScript 实现的最后说明

虽然实现混合方法很有趣,但调试 Javascript 却很痛苦,通常需要使用浏览器调试器或 ClearScript 控制台窗口。显然,Javascript 可以通过浏览器进行调试,但我没有深入研究这一点。我也不知道所有这些的性能如何,因此在使用 ClearScript 为 C# 应用程序添加脚本时,请购买者自负!另外,花了几小时才弄清楚以我使用 ClearScript 对象的方式工作时的细微差别,最终需要“Keys”这种笨拙的方法。所以,这项技术很酷,它确实运行得很好,但我不认为这是你想要向你的团队负责人汇报并说“我找到了一个向 C# 添加脚本的好方法!”的事情。我建议你看看 CPian JohnLeitch 关于 Aphid 的文章。

纯 Javascript 实现

既然您已经看到了纯 C# 和混合 C# - Javascript 实现,那么让我们用纯 Javascript 实现来总结一下。很棒的是,我们之前为映射和条件-映射匹配创建的 Javascript 脚本在纯 Javascript 实现中是 100% 可用的。

关于 Javascript 实现有几点需要注意

  1. 注册代理不再需要在 Keys 属性中指定返回对象模式,因为,嗯,它是 Javascript!
  2. Javascript 是单线程的,但某些操作,如执行 HTTP GET 或休眠,是异步的。这并不意味着它们在单独的线程上运行(至少在脚本处理中),而是意味着当它们完成时,处理会恢复。这意味着如果数据流处理器在没有事情可做的情况下退出,我们必须重新启动它。我不想处理自己实现信号量机制,所以它有点像一个 hack。
  3. 我正在使用邪恶的 eval 语句。目前它能工作。请记住,我们最终希望用户能够为已编码的代理编写映射和条件的脚本,因此我们将不得不处理字符串而不是编写函数。这似乎有点晦涩,因为我们一开始就用 Javascript 编写所有内容,但请记住,下一步(在下一篇文章中)是将代理可视化地放置在数据流图上,并简单地编写重要的映射、归约、过滤、匹配和输出函数。

Javascript 实现的优点在于所有东西都是 JSON 对象,C# 实现中必须经历的与 ExpandoObjects 相关的所有奇怪的麻烦都消失了。我不得不说 Javascript 在与 JSON 和字典键值数据无缝协作方面确实表现出色,这使其成为实现这个概念的天然语言。

为什么我需要服务器来运行演示?

因为我们使用 XMLHttpRequest 发出 HTTP GET 请求,如果我们将 HTML 文件简单地加载到 Chrome 浏览器窗口中,我们会得到

'Access-Control-Allow-Origin' header has a value 'null' that is not equal to the supplied origin.
Origin 'null' is therefore not allowed access.

呸。这意味着我们必须通过服务器托管我们的 HTML 页面及其 Javascript。一个简单的方法是启动内置的 Python 服务器。如果您安装了 Python,请在包含“apodSlideShow.html”文件的目录中打开一个控制台窗口——一种方法是在 Windows 中导航到该文件夹,然后在地址栏中键入“cmd”并按 Enter。然后,根据您拥有的 Python 版本

Python 2.x

python -m SimpleHTTPServer

Python 3.x

python -m http.server

然后您可以导航到 localhost:8000/apodSlideShow.html 来启动幻灯片。

不要使用 Edge

不知何故,Edge 弹出了这个对话框

我已经在 JsFiddle、Chrome 和 Firefox 中测试了该应用程序,它在 Python 服务器上运行良好。但当然,在 Edge 中不行。据称这是因为缺少“scheme”,所以如果您尝试 https://:8000/apodSlideShow.html,HTTP 请求永远不会完成。这是控制台日志

注意状态是0(readyState是4)

记录回调,我们看到

根据这份文档 https://mdn.org.cn/en-US/docs/Web/Guide/AJAX/Getting_Started,这似乎是问题所在

第二个参数是您发送请求的 URL。作为一项安全功能,默认情况下您无法调用第三方域上的 URL。请务必在所有页面上使用完全相同的域名,否则在调用 open() 时会收到“权限被拒绝”错误。

话又说回来,这可能也与本地 intranet / 本地 intranet 区域设置有关。我不知道,我也不想浪费时间找出 Edge 的问题,因为它是唯一一个不能“正确”工作的浏览器。如果有人有解决方案,请在本文章的评论部分留言。

数据流队列的初始化和处理

function processQueue() {
  while (app.queue.length > 0) {
    let data = app.queue.shift();
    agents.filter(agent => agent.context == data.context && agent.dataType == data.type).
    map(agent => {
      console.log("Invoking " + agent.constructor.name + " : " + data.context + "." + data.type);
      agent.call(app.context, data);
    });
  }
}

let app = initializeApp();
let agents = instantiateAgents();
processQueue();

请注意,日志记录看起来与 C# 输出几乎相同

代理

这是不同代理的实现。这些应该与 C# 对应物非常相似。但是,您会注意到没有 HttpGetImage 代理,因为这被委托给一个简单的“输出”代理,将图像源映射到 HTML 中的 URL。

映射代理

class MapAgent extends Agent {
  constructor(context, dataType, responseDataType, responseContext) {
    super(context, dataType, responseDataType, responseContext);
    this.useAgentContextAndType = true;
  }

  call(context, data) {
    let fncMap = ((data.map === undefined) ? this.map : data.map);
    let resp = eval(fncMap);
    this.setContextAndType(data, resp, this.useAgentContextAndType);
    publish(resp);
  }
}

匹配代理

class MatchAgent extends Agent {
  constructor(context, dataType, responseDataType, responseContext) {
    super(context, dataType, responseDataType, responseContext);
    this.matches = [];

    return this;
  }

  add(condition, map, useAgentContextAndType = true) {
    this.matches.push({ condition: condition, map: map, useAgentContextAndType: useAgentContextAndType });

    return this;
  }

  call(context, data) {
    for (let i = 0; i < this.matches.length; i++) {
      if (eval(this.matches[i].condition)) {
        let resp = eval(this.matches[i].map);
        this.setContextAndType(data, resp, this.matches[i].useAgentContextAndType);
        publish(resp);
        break;
      }
    }
  }
}

输出代理

class OutputAgent extends Agent {
  constructor(context, dataType, responseDataType, responseContext, action) {
    super(context, dataType, responseDataType, responseContext);
    this.action = action;
  }

  call(context, data) {
    this.action(context, data);
    this.setContextAndType(data, data);
    publish(data);
  }
}

休眠代理

class SleepAgent extends Agent {
  constructor(context, dataType, responseDataType, responseContext, ms) {
    super(context, dataType, responseDataType, responseContext);
    this.ms = ms;
  }

  sleep(time) {
    return new Promise((resolve) => setTimeout(resolve, time));
  }

  call(context, data) {
    this.sleep(this.ms).then(() => {
      this.setContextAndType(data, data);
      // Sleep is an async function, so we need to kick the data pool processor.
      publish(data, true);
    });
  }
}

HttpGetJsonAgent

class HttpGetJsonAgent extends Agent {
  constructor(context, dataType, responseDataType, responseContext) {
    super(context, dataType, responseDataType, responseContext);
  }

  call(context, data) {
    let req = new XMLHttpRequest();
    req.onreadystatechange = publishResponse(this, req, data);
    req.open("GET", data.url);
    req.send();
  }
}

function publishResponse(agent, req, data) {
  // Closure:
  let myagent = agent;
  let myreq = req;
  let mydata = data;
  return function() {
    if (this.readyState == 4 && this.status == 200) {
      let resp = JSON.parse(myreq.responseText);
      myagent.setContextAndType(mydata, resp);
      publish(resp, true);
    }
  }
}

实例化代理

这是整个函数——如果你已经阅读了上面的 C# 实现,我想我不需要将其拆分成单独的代码片段。输出代理目前被实现为需要一个函数来更新 HTML 的代理。我需要在可视化设计器中实现这个概念时处理这个问题。

function instantiateAgents() {
  let agents = [];

  let apodUrlMappingAgent = new MapAgent("APOD", "ApodRequestData", "ApodUrl");
  apodUrlMappingAgent.map = "({ url: context.Url + '?api_key=' + context.ApiKey + '&date=' + data.date })";

  let httpGetJsonAgent = new HttpGetJsonAgent("APOD", "ApodUrl", "ApodResponseData");

  let apodMediaTypeFilter = new MatchAgent("APOD", "ApodResponseData", "ApodNotVideo").
  add("data.media_type == 'image'", "data").
  add("true", "({type: 'Date', date: data.date})", false);

  let apodTextToControlAgent = new OutputAgent("APOD", "ApodNotVideo", undefined, undefined, (context, data) => {
    document.getElementById("date").innerHTML = data.date;
    document.getElementById("title").innerHTML = data.title;
    document.getElementById("explanation").innerHTML = data.explanation;
  });

  let apodBestImageAgent = new MatchAgent("APOD", "ApodNotVideo", "ImageUrl").
    add("data.hdurl !== undefined && data.hdurl != ''", "({url: data.hdurl, date: data.date})").
    add("true", "({url: data.url, date: data.date})");

  let apodImageAgent = new OutputAgent("APOD", "ImageUrl", "ApodDelay", undefined, (context, data) => {
    document.getElementById("image").innerHTML = "<image src='" + data.url + "'>";
  });

  let sleepAgent = new SleepAgent("APOD", "ApodDelay", "DateCheck", undefined, 10000);

  let dateCheckAgent = new MatchAgent("APOD", "DateCheck", "Date").
    add("new Date(data.date + ' 23:59') < Date.now()", "data");

  let apodNextDateAgent = new MapAgent("APOD", "Date", "NextDate");
  apodNextDateAgent.map = "var nextDate = new Date(data.date + ' 0:01'); 
    nextDate.setDate(nextDate.getDate() + 1); 
   ({ date: nextDate.getFullYear() + '-' + 
     ('0' + (nextDate.getMonth() + 1)).slice(-2) + '-' + 
     ('0' + nextDate.getDate()).slice(-2) })";

  let apodNextImageAgent = new MapAgent("APOD", "NextDate", "ApodRequestData");
  apodNextImageAgent.map = "({date: data.date})";

  agents.push(apodUrlMappingAgent);
  agents.push(httpGetJsonAgent);
  agents.push(apodMediaTypeFilter);
  agents.push(apodNextDateAgent);
  agents.push(apodNextImageAgent);
  agents.push(apodTextToControlAgent);
  agents.push(apodBestImageAgent);
  agents.push(apodImageAgent);
  agents.push(sleepAgent);
  agents.push(dateCheckAgent);

  return agents;
}

异步细微差别

这里笨拙的部分是当异步操作完成时,数据流处理器必须重新启动。此时,有两个异步并触发回调的代理:HTTP GET 代理和休眠代理。所以,当我们发布响应日期时,我们必须这样做

function publish(data, fromAsyncCall = false) {
  app.queue.push(data);

  // If the queue is empty, the loop has exited, so restart the queue processing.
  if (fromAsyncCall && app.queue.length == 1) {
    processQueue();
  }
}

好吧。它奏效了。

结论

下一步是将其整合到 FlowSharpWeb 设计器中。这将真正凸显以数据为中心的代理进行可视化编程的概念。不再编写 if-then-else、while 等语句,也不再使用仅仅是语言流程控制对象的块来“可视化”编程它们,而是利用映射、归约和过滤的概念,以及匹配等函数式编程概念,从而形成一个更高层次的可视化编程工具。最终的想法是,您可以使用映射/过滤/归约/匹配 + “自定义”代理构建模块化组件,然后从这些模块构建更复杂的应用程序,将应用程序构建成一个三维应用程序,其中二维表面是模块之间的交互,第三维是每个模块、子模块等的详细实现。

© . All rights reserved.