Windows UI 自动化:从 C# ListBox 控件中获取选定的对象
Windows UI Automation: Get selected object from C# ListBox control
一点背景知识:我目前正在使用 Winforms/C# 编写一个模拟 Conway's Game of Life. Part of this sample involves UI Automation using the White Automation Framework 的示例项目。窗体的相关布局包括设置世界的自定义网格控件和displays/stores历代世界的列表框控件。
我有一个 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.PreviousGenerations
是 World
个对象的集合。
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);
}
}
我遇到的问题是测试 class 中的事件处理程序代码从未被调用。我认为问题出在 AutomationInteropProvider.HostProviderFromHandle
调用返回与测试代码中的提供程序对象不同的提供程序对象,但我不确定。
我的问题是:
- 我可以采用更好的方法吗,例如 MS Automation API 提供的方法?
- 如果没有 - 有没有办法获得 ListBox 控件的默认 C#
IRawElementProviderSimple
实现(以引发 属性 Changed 事件)?我宁愿不为了这一点功能而重新实现它。
这是测试端的代码,它添加了 ItemStatus属性 更改事件的侦听器。我正在为 BDD 使用 SpecFlow,它将 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);
}
}
我能够通过使用 non-persisted memory mapped files 允许 window GUI 和测试进程之间的额外通信来解决这个问题。
这最终比尝试为我的 "custom" ListBox 和其中包含的项目完全重写 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);
}
一点背景知识:我目前正在使用 Winforms/C# 编写一个模拟 Conway's Game of Life. Part of this sample involves UI Automation using the White Automation Framework 的示例项目。窗体的相关布局包括设置世界的自定义网格控件和displays/stores历代世界的列表框控件。
我有一个 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.PreviousGenerations
是 World
个对象的集合。
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);
}
}
我遇到的问题是测试 class 中的事件处理程序代码从未被调用。我认为问题出在 AutomationInteropProvider.HostProviderFromHandle
调用返回与测试代码中的提供程序对象不同的提供程序对象,但我不确定。
我的问题是:
- 我可以采用更好的方法吗,例如 MS Automation API 提供的方法?
- 如果没有 - 有没有办法获得 ListBox 控件的默认 C#
IRawElementProviderSimple
实现(以引发 属性 Changed 事件)?我宁愿不为了这一点功能而重新实现它。
这是测试端的代码,它添加了 ItemStatus属性 更改事件的侦听器。我正在为 BDD 使用 SpecFlow,它将 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);
}
}
我能够通过使用 non-persisted memory mapped files 允许 window GUI 和测试进程之间的额外通信来解决这个问题。
这最终比尝试为我的 "custom" ListBox 和其中包含的项目完全重写 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);
}