Windows UI自动化:从C#列表框控件获取选定对象

Windows UI自动化:从C#列表框控件获取选定对象,c#,winforms,automation,microsoft-ui-automation,white-framework,C#,Winforms,Automation,Microsoft Ui Automation,White Framework,一点背景:我目前正在使用Winforms/C编写一个模拟的示例项目。此示例的一部分涉及使用的UI自动化。表单的相关布局包括一个用于设置世界的自定义网格控件和一个用于显示/存储过去几代世界的列表框控件 我有一个World对象,它存储Cell对象的列表,并根据当前状态计算下一代World: public class World { public IReadOnlyCollection<Cell> Cells { get; private set; } public Worl

一点背景:我目前正在使用Winforms/C编写一个模拟的示例项目。此示例的一部分涉及使用的UI自动化。表单的相关布局包括一个用于设置世界的自定义网格控件和一个用于显示/存储过去几代世界的列表框控件

我有一个
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()
   {
      /* ... */
   }
}
从这个片段中,您可以看到ListBox的项是世界对象。我想在测试代码中做的是从所选列表框项中获取实际的
World
对象(或其某些表示),然后将其与网格的世界表示进行比较。网格有一个完全自动化的实现,因此我可以使用现有的白色自动化调用轻松地获得网格的表示

我唯一的想法是创建一个派生的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
调用返回的提供程序对象与测试代码中的不同,但我不确定

我的问题是:

  • 我是否可以采取更好的方法,例如MS Automation API提供的方法
  • 如果没有-是否有办法获取ListBox控件的默认C#
    irawlementProviderSimple
    实现(以引发属性更改事件)?我不希望仅仅为了这一点功能而重新实现它
  • 下面是来自测试端的代码,它添加了ItemStatusProperty更改事件的侦听器。我正在使用SpecFlow for BDD,它将
    ScenarioContext.Current
    定义为字典
    WorldGridSteps.Window
    是一个
    TestStack.White.Window
    对象

      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);
         }
      }
    
    private static void HookListItemStatusEvent()
    {
    var list=WorldGridSteps.Window.Get(GENERATION\u list\u NAME);
    Automation.AddAutomationPropertyChangedEventHandler(list.AutomationElement,
    树镜元素,
    每一代人当选,
    AutomationElementIdentifiers.ItemStatusProperty);
    }
    私有静态void UnhookListItemStatusEvent()
    {
    var list=WorldGridSteps.Window.Get(GENERATION\u list\u NAME);
    Automation.RemoveAutomationPropertyChangedEventHandler(list.AutomationElement,OnGenerationsSelected);
    }
    私有静态void OnGenerationSelected(对象发送方,AutomationPropertyChangedEventArgs e)
    {
    if(e.EventId.Id!=AutomationElementIdentifiers.ItemStatusProperty.Id)
    返回;
    世界=空;
    开关(如字符串形式的旧值)
    {
    case PastGenerationListBox.ITEMSTATUS\u SELECTEDITEMCHANGED:
    world=WorldSerialize.DeserializeWorldFromString(例如,将NewValue作为字符串);
    打破
    }
    如果(世界!=null)
    {
    if(ScenarioContext.Current.ContainsKey(选定的世界键))
    ScenarioContext.Current[所选世界\u键]=世界;
    其他的
    ScenarioContext.Current.Add(选中的世界键,世界);
    }
    }
    
    我通过允许在窗口GUI和测试过程之间进行额外的通信,解决了这个问题

    这比尝试为我的“自定义”列表框和其中包含的项目完全重新编写
    IRawElementProviderSimple
    实现要容易得多

    我的自定义列表框最终如下所示:

    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);
    }
    
    我的测试代码如下所示:

    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);
    }