Автоматизация пользовательского интерфейса Windows: получение выбранного объекта из элемента управления C# ListBox

Немного предыстории: в настоящее время я пишу пример проекта с использованием Winforms/C#, который эмулирует Игру жизни Конвея< /а>. Часть этого примера включает автоматизацию пользовательского интерфейса с использованием White Automation Framework. Соответствующий макет формы включает в себя настраиваемый элемент управления сеткой для настройки мира и элемент управления списком, который отображает/сохраняет прошлые поколения мира.

У меня есть объект 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()
   {
      /* ... */
   }
}

В моем пользовательском интерфейсе, когда я вычисляю следующее поколение мира, список прошлых поколений обновляется. Список прошлого поколения хранит World объектов в качестве своих элементов, и я подписался на событие Format списка для форматирования отображения элементов. _worldProvider.PreviousGenerations — это набор World объектов.

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

Из этого фрагмента видно, что элементы ListBox являются объектами World. Что я хочу сделать в своем тестовом коде, так это получить фактический объект World (или некоторое его представление) из выбранного элемента ListBox, а затем сравнить его с представлением мира в сетке. В сетке реализована полная автоматизация, поэтому я могу легко получить представление о сетке, используя существующие вызовы автоматизации в белом цвете.

Единственная идея, которая у меня была, заключалась в том, чтобы создать производный элемент управления 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. Есть ли лучший подход, который я могу использовать, например, что-то, предоставляемое MS Automation API?
  2. Если нет, есть ли способ получить реализацию С# IRawElementProviderSimple по умолчанию для элемента управления ListBox (чтобы вызвать событие Property Changed)? Я бы предпочел не реализовывать его повторно только для этой небольшой функциональности.

Вот код со стороны теста, который добавляет прослушиватель для события изменения ItemStatusProperty. Я использую SpecFlow для 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);
     }
  }

person E. Moffat    schedule 16.12.2015    source источник


Ответы (1)


Мне удалось обойти эту проблему с помощью не- отображаемые файлы постоянной памяти, чтобы обеспечить дополнительную связь между графическим интерфейсом окна и процессом тестирования.

Это оказалось намного проще, чем пытаться полностью переписать IRawElementProviderSimple реализации как для моего "настраиваемого" ListBox, так и для элементов, содержащихся внутри.

Мой пользовательский 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);
}
person E. Moffat    schedule 17.12.2015