Windows UI自动化:从C#ListBox控件中获取所选对象

本文关键字:获取 对象 控件 C#ListBox UI 自动化 Windows | 更新日期: 2023-09-27 18:27:07

一个小背景:我目前正在使用Winforms/C#编写一个示例项目,该项目模拟了Conway的《生命的游戏》。此示例的一部分涉及使用White Automation Framework的UI Automation。表单的相关布局包括一个用于设置世界的自定义网格控件和一个显示/存储过去几代世界的列表框控件。

我有一个World对象,它存储了Cell对象的列表,并根据其当前状态计算下一代World

public class World
{
   public IReadOnlyCollection<Cell> Cells { get; private set; }
   public World(IList<Cell> seed)
   {
      Cells = new ReadOnlyCollection<Cell>(seed);
   }
   public World GetNextGeneration()
   {
      /* ... */
   }
}

在我的UI中,当我计算下一代世界时,上一代列表会更新。上一代列表将World对象存储为其项目,我已经订阅了列表框的Format事件以格式化项目显示。_worldProvider.PreviousGenerationsWorld对象的集合。

private void UpdatePastGenerationsList()
{
   GenerationList.SuspendLayout();
   GenerationList.Items.Add(_worldProvider.PreviousGenerations.Last());
   GenerationList.SelectedItem = _worldProvider.PreviousGenerations.Last();
   GenerationList.ResumeLayout();
}

从这个片段中,您可以看到ListBox的项是World对象。在我的测试代码中,我想做的是从所选的ListBox项中获得实际的World对象(或它的某种表示),然后将其与网格对世界的表示进行比较。网格有一个完全自动化的实现,所以我可以使用White中的现有自动化调用轻松地获得网格的表示。

我唯一的想法是制作一个派生的ListBox控件,当所选索引从自动化单击事件更改时,该控件发送一个ItemStatus属性更改的自动化事件,然后在测试代码中侦听该ItemStatus事件。世界首先被转换为字符串(WorldSerialize.SerializeWorldToString),其中每个活细胞被转换为格式化的坐标{x},{y};

public class PastGenerationListBox : ListBox
{
   public const string ITEMSTATUS_SELECTEDITEMCHANGED = "SelectedItemChanged";
   protected override void OnSelectedIndexChanged(EventArgs e)
   {
      FireSelectedItemChanged(SelectedItem as World);
      base.OnSelectedIndexChanged(e);
   }
   private void FireSelectedItemChanged(World world)
   {
      if (!AutomationInteropProvider.ClientsAreListening)
         return;
      var provider = AutomationInteropProvider.HostProviderFromHandle(Handle);
      var args = new AutomationPropertyChangedEventArgs(
                      AutomationElementIdentifiers.ItemStatusProperty,
                      ITEMSTATUS_SELECTEDITEMCHANGED,
                      WorldSerialize.SerializeWorldToString(world));
      AutomationInteropProvider.RaiseAutomationPropertyChangedEvent(provider, args);
   }
}

我遇到的问题是,从来没有调用过测试类中的事件处理程序代码。我认为问题在于AutomationInteropProvider.HostProviderFromHandle调用返回的提供程序对象与测试代码中的不同,但我不确定。

我的问题是:

  1. 有没有更好的方法可以让我采取,比如微软自动化API提供的方法
  2. 如果不是-有没有办法让ListBox控件获得默认的C#IRawElementProviderSimple实现(引发Property Changed事件)?我宁愿不只是为了这一点点功能而重新实现它

这是来自测试端的代码,它为ItemStatusProperty更改事件添加了侦听器。我正在为BDD使用SpecFlow,它将ScenarioContext.Current定义为字典。CCD_ 15是一个CCD_。

  private static void HookListItemStatusEvent()
  {
     var list = WorldGridSteps.Window.Get<ListBox>(GENERATION_LIST_NAME);
     Automation.AddAutomationPropertyChangedEventHandler(list.AutomationElement,
                                                         TreeScope.Element,
                                                         OnGenerationSelected,
                                                         AutomationElementIdentifiers.ItemStatusProperty);
  }
  private static void UnhookListItemStatusEvent()
  {
     var list = WorldGridSteps.Window.Get<ListBox>(GENERATION_LIST_NAME);
     Automation.RemoveAutomationPropertyChangedEventHandler(list.AutomationElement, OnGenerationSelected);
  }
  private static void OnGenerationSelected(object sender, AutomationPropertyChangedEventArgs e)
  {
     if (e.EventId.Id != AutomationElementIdentifiers.ItemStatusProperty.Id)
        return;
     World world = null;
     switch (e.OldValue as string)
     {
        case PastGenerationListBox.ITEMSTATUS_SELECTEDITEMCHANGED:
           world = WorldSerialize.DeserializeWorldFromString(e.NewValue as string);
           break;
     }
     if (world != null)
     {
        if (ScenarioContext.Current.ContainsKey(SELECTED_WORLD_KEY))
           ScenarioContext.Current[SELECTED_WORLD_KEY] = world;
        else
           ScenarioContext.Current.Add(SELECTED_WORLD_KEY, world);
     }
  }

Windows UI自动化:从C#ListBox控件中获取所选对象

我能够通过使用非持久内存映射文件来解决这个问题,从而允许在窗口GUI和测试进程之间进行额外的通信。

这最终比尝试为我的"自定义"ListBox和其中包含的项完全重写IRawElementProviderSimple实现要容易得多。

我的自定义ListBox最终看起来是这样的:

public class PastGenerationListBox : ListBox
{
   public const string SELECTEDWORLD_MEMORY_NAME = "SelectedWorld";
   public const string SELECTEDWORLD_MUTEX_NAME = "SelectedWorldMutex";
   private const int SHARED_MEMORY_CAPACITY = 8192;
   private MemoryMappedFile _sharedMemory;
   private Mutex _sharedMemoryMutex;
   public new World SelectedItem
   {
      get { return base.SelectedItem as World; }
      set { base.SelectedItem = value; }
   }
   public PastGenerationListBox()
   {
      _sharedMemory = MemoryMappedFile.CreateNew(SELECTEDWORLD_MEMORY_NAME, SHARED_MEMORY_CAPACITY);
      _sharedMemoryMutex = new Mutex(false, SELECTEDWORLD_MUTEX_NAME);
   }
   protected override void OnSelectedIndexChanged(EventArgs e)
   {
      WriteSharedMemory(SelectedItem);
      base.OnSelectedIndexChanged(e);
   }
   protected override void Dispose(bool disposing)
   {
      if (disposing)
      {
         _sharedMemoryMutex.WaitOne();
         if (_sharedMemory != null)
            _sharedMemory.Dispose();
         _sharedMemory = null;
         _sharedMemoryMutex.ReleaseMutex();
         if (_sharedMemoryMutex != null)
            _sharedMemoryMutex.Dispose();
         _sharedMemoryMutex = null;
      }
      base.Dispose(disposing);
   }
   private void WriteSharedMemory(World world)
   {
      if (!AutomationInteropProvider.ClientsAreListening) return;
      var data = WorldSerialize.SerializeWorldToString(world);
      var bytes = Encoding.ASCII.GetBytes(data);
      if (bytes.Length > 8188)
         throw new Exception("Error: the world is too big for the past generation list!");
      _sharedMemoryMutex.WaitOne();
      using (var str = _sharedMemory.CreateViewStream(0, SHARED_MEMORY_CAPACITY))
      {
         str.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
         str.Write(bytes, 0, bytes.Length);
      }
      _sharedMemoryMutex.ReleaseMutex();
   }
}

我的测试代码如下:

private static World GetWorldFromMappedMemory()
{
   string str;
   using (var mut = Mutex.OpenExisting(PastGenerationListBox.SELECTEDWORLD_MUTEX_NAME))
   {
      mut.WaitOne();
      using (var sharedMem = MemoryMappedFile.OpenExisting(PastGenerationListBox.SELECTEDWORLD_MEMORY_NAME))
      {
         using (var stream = sharedMem.CreateViewStream())
         {
            byte[] rawLen = new byte[4];
            stream.Read(rawLen, 0, 4);
            var len = BitConverter.ToInt32(rawLen, 0);
            byte[] rawData = new byte[len];
            stream.Read(rawData, 0, rawData.Length);
            str = Encoding.ASCII.GetString(rawData);
         }
      }
      mut.ReleaseMutex();
   }
   return WorldSerialize.DeserializeWorldFromString(str);
}