If prompted right now, could you explain the design of the software that you’re working on, and the change that you’re making to it? What about the other members of your team? For our team, the answer was no, and a debate over the cause of that failure ultimately led us to implementing daily design workshops which have not only solved the problem, but produced several other unintended benefits.
“What is the cause?” we immediately debated as it became clear that the team couldn’t explain what they were actively doing, even what they had been doing over the course of a week. I argued that there was a failure of commination skills rather than a failure in understanding. The code changes themselves were generally correct, and consistent with the design agreed upon during sprint planning. Others argued that truly understand an idea is synonymous with being able to communicate that idea, a belief I find troublesome after years of studying language. Code itself is a form of communication, but in a collaborative environment it can be difficult to parse out who understands what from the software alone. Each team member must be able to articulate their understanding.
And so, with one party concerned that the team did not understand the software that they were building, and myself concerned that the team lacked the communication skills to adequately convey their knowledge, we decided upon a strategy to address both concerns.
Each morning, after our daily standup, one team member is tasks with explaining the design for the story at a whiteboard, or for the test automation for that story. The rest of the team is responsible for asking questions until any ambiguities are resolved, and a mutual understanding is achieved. The exercise usually takes about fifteen minutes.
In this way, each team member gains practice discussing and diagraming software design. They’re improving their communication skills through practice, and by observing the techniques of other team members.
We are also ensuring a mutual understanding of the software design with daily discussion and reinforcement of that design. Discoveries made during coding, and modifications to the original plan are raised daily and shared with the entire team within the context of the overall design.
When we discover a challenging problem or an unknown behavior in the software that needs to be discussed with the entire team, this topic is raised during the design workshop instead of the standard review of the story. We gain a mutual awareness of issues, and collective problem solving.
We have also been surprised to see that the side effect of this is faster, more efficient stand-ups. Team members no longer use the stand-up as an opportunity to share difficulties or discoveries because the design workshop is a better forum for those topics.
Ultimately, we’ve learned that we had both problems of communication and understanding specific to certain individuals and areas of the software. Fortunately, we have a working plan for addressing both issues, and we’ve observed steady progress toward resolving these difficulties.
For six months now, I’ve been building a team to take on the re-engineering of a legacy code base for scalability, performance, and to provide flexibility in the core processing components to accommodate differences in global regulatory requirements. To take on this project, I have a smart, but largely inexperienced team. I am finding my usual strategy for training, and professionalizing new developers insufficient, so I’ve decided to try something new. This is the beginning of my experiment.
Here’s what I’ve tried so far: TDD katas running the standard exercises, String Calculator, Bowling Game, etc. I’ve recommended reading Working Effectively with Legacy Code, Clean Code, and The Pragmatic Programmer. I promote stewardship of the code. Even my most junior team member is expected to take responsibility for tasks, review the code of other team members, and make decisions about how to implement and test features. It’s a safe team to make decisions, and to make mistakes.
In a lot of ways, this approach is working. The team is increasingly effective and independent. We’re delivering code, and testing thoroughly in sprint. But, in many ways, we’re falling short of our goals. Overall confidence on the team is low. The quantity and severity of changes required following code reviews, or as a result of defects discovered during test is driving an impulse to seek approval before committing to low-level design decisions. Knowledge silos build up extremely quickly as an individual (or pair) learn how to interact with the legacy code or framework, then take all similar subsequent tasks to accelerate completion of a story, or sprint. We’re also failing sprints due to a combination of poor estimation, and defects discovered during testing.
I believe that the root of our problem lies in our attempt to work both on a new type of problem, and on that problem in an extremely complex code base. For example, the team recently introduced MyBatis to handle new database interactions instead of continuing the use of a custom DAO framework oriented around stored procedures. For everyone on the team, MyBatis is a new tool, but they’re also trying to integrate this with the WebLogic data sources, to replicate behavior in code, such as transactional updates and audit trails, which have typically been provided by the stored procedures, and to do all of this with complex objects.
There’s too much newness here. As a result, the team has struggled to break big problems down into smaller components. They’re not sure if the failure of their MyBatis DAO is related to the MyBatis configuration, the JNDI lookup of the data source, and since both tools are new, they didn’t know how to configure one at a time and test.
I need experience that I don’t have, so I’ve decided to try and produce it artificially. Starting next sprint, I’m replacing my TDD katas with targeted exercises to practice simplified versions of problems that we’ll be facing in the future. I’m going to take my real code, pare it down, and then present a piece of the upcoming problem as a TDD exercise. I might ask the team to replace a stored procedure with a MyBatis DAO, but the procedure itself will be simple and clear, the objects small, and I’ll provide explicit connection details rather than a JNDI lookup on a WebLogic server. We’ll still test first, but we may need to first refactor for testability, and we’ll still throw away our code at the end of the exercise.
The result, I hope, will be that when we encounter the problem in our code, the team will have experience with at least some part of the solution. We’ll reduce our unknowns, and we’ll know how to break the problem into smaller pieces because we will have experienced a small piece already. We will address our complexity as complexity, and avoid getting stuck on simple problems concealed by complex code and interactions.
I recently created sample code for testing JMS inputs and outputs using Cucumber-JVM. The full code is available on github at https://github.com/wjpowell/cucumber-jvm-jms-example. The code relies on a reusable class which I’ve set up to simplify interaction with a JMS queue, use of @BeforeClass and @AfterClass hooks for setup and teardown of test suites, and, because data loads are so often a part of testing middleware, this loads trade data from an excel sheet using the jXLS library. I can’t cover all of these features in one blog post, so for now I’m going to focus on the JMS components
The entire JMS utilities class used in the Step Definitions is included below. For convenience, this class only uses the javax.jms interfaces. This allows any connection factory to be injected, and used in the same way. The class has been structured to allow users to simply connect to a JMS server, and to create Message producers and message receivers for interacting with that server.
While most of this code is boilerplate setup and teardown of a JMS server, the createTestListener deserves some attention. This method accepts a collection as a parameter, and adds received messages to that collection. It does so by implementing an anonymous MessageListener, which can be attached to the outbound queue.
package cucumber.examples.java.jms.utilities;
public <T> MessageListener createTestListener(
final List<T> receivedCollection) {
MessageListener testListener = new MessageListener() {
@SuppressWarnings("unchecked")
@Override
public void onMessage(Message received) {
if (received instanceof ObjectMessage) {
try {
receivedCollection.add((T) ((ObjectMessage) received)
.getObject());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
};
return testListener;
}
This message listener is attached to the outbound queue before any messages are sent. The code then loads the messages, and sends them through the input queue, and waits until all expected messages have been received before checking the assertions.
private void waitUntilAllOutputIsReceived(List<Trade> receivedTrades, int expectedTrades, int timeout) throws InterruptedException {
int timeoutReached = 0;
while (receivedTrades.size() != expectedTrades && timeoutReached < timeout) {
Thread.sleep(2000);
timeoutReached += 2000;
}
}
Here is the full class for the JMS utilities.
package cucumber.examples.java.jms.utilities;
public class JmsTestUtilities {
private Session session;
private Connection connection;
private ConnectionFactory connectionFactory;
public JmsTestUtilities(ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
public Queue createQueue(String queueName) throws JMSException {
return session.createQueue(queueName);
}
public void sendMessage(Serializable object, MessageProducer producer) {
try {
// Create a messages
ObjectMessage message = session.createObjectMessage(object);
producer.send(message);
} catch (Exception e) {
System.out.println("Caught: " + e);
e.printStackTrace();
}
}
public void closeSession() throws JMSException {
session.close();
connection.close();
}
public void startServerSession() throws JMSException {
connection = connectionFactory.createConnection();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
connection.start();
}
public MessageProducer createInputProducer(Queue inputDestination)
throws JMSException {
MessageProducer producer = session.createProducer(inputDestination);
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
return producer;
}
public MessageConsumer createOutputReceiver(String outputQueue)
throws JMSException {
Queue outputDestination = session.createQueue(outputQueue);
MessageConsumer outputConsumer = session
.createConsumer(outputDestination);
return outputConsumer;
}
public void attachListenerToOutputConsumer(MessageListener testListener,
MessageConsumer outputConsumer) throws JMSException {
outputConsumer.setMessageListener(testListener);
}
public <T> MessageListener createTestListener(
final List<T> receivedCollection) {
MessageListener testListener = new MessageListener() {
@SuppressWarnings("unchecked")
@Override
public void onMessage(Message received) {
if (received instanceof ObjectMessage) {
try {
receivedCollection.add((T) ((ObjectMessage) received)
.getObject());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
};
return testListener;
}
public Session getSession() {
return session;
}
}
I’m getting started on a Gem which provides a collection of PageObject widgets for testing Gxt Applications. I’m working my way through the examples provided on the Gxt Example Gallery. The project is up and running on GitHub, and I’m looking for contributors. I’ll release the first few widgets to RubyGems soon.
The goal of building this library are twofold. First, I’d like to provide a library for testing the standard Gxt Controls, which can be used directly, or easily extended. Second, I’d like to provide example code for creating Widget extensions for PageObject.
Testing Gxt sites has proven unusually challenging because of the complexity of the HTML generated by its controls, as such it is particularly well suited as the object of a test library. It’s a popular library which is difficult to test, so the simplification of testing has a big payoff for those development teams working in the technology.
I have already provided simple, straightforward examples of creating widgets for PageObject. I hope that examples of the more challenging widgets required for testing Gxt will help those developers working with complex custom controls when they look to define their own test widgets.
I was recently helping a team start testing their application using SpecFlow and Coded UI. They were also using TeamCity as a continuous integration server. Setting up SpecFlow to run Coded UI tests is well documented on the Specflow Wiki. Running SpecFlow tests in Teamcity has been well covered in this blog post by Hamish Graham. The challenge that we encountered was in running UI Tests on locked computers.
It is not possible within our organization to disable screensavers, which prevent UI tests from running. To get around this, we set up several VMs using VMware Player. on physical servers and installed TeamCity build agent on them. The VMs were configured without screen savers. Port forwarding was set up to allow connection to the build agent, in addition to several ports for the applications to communicate with their servers.
The TeamCity build agent can be set up to run as an interactive process (launch it from the command line rather than as a service), or the Microsoft Test Controller and Test Agent can be used to allow distribution of the test run across several VMs. Make sure to run the Test Agent as an interactive process, if you’re going to run the tests in this way. You’ll need additional port forwarding set up for the test controller and agents if you choose to use the Microsoft test controller.
Each VM must be open on it’s machine to keep the desktop session alive. If the VM is open, the physical machine can be safely locked. Remoting into the VM Directly will cause the desktop session to be closed on log out, so make sure that any users needing access to the VM running the tests are able to connect to the physical machine.
Power users know all the shortcuts in an application, they’re never more than a few clicks away from what they want. They keep their hands on they keyboard. User interface tests, on the other hand, tend to click their way through an application, following the main routes. When practicing BDD, it is important to write tests for these main routes, but it is also important to script the majority of the tests like a power user or you’ll wind up with slow running tests, and worse, applications that do not reward expertise.
Web application tests can be accelerated by streamlining workflows, and by improving navigation around the site. The main speed loss in Web application testing is navigation between pages, particularly when tests are written to run in isolation.
Windows CodedUI tests lose most of their time searching for controls on complex applications. Allowing applications to be navigated using keyboard shortcuts can dramatically reduce the run time for tests.
Applications on any platform should allow values to be typed into combo boxes and date selectors. Tab index should be properly configured. It should be possible to complete forms quickly with the keyboard.
When UI tests are running slow, look at the underlying code, but also look at the usability of the application. Slow running tests may indicate a usability problem. Improving the navigation of the application will speed up your tests, and make your users happy.
I have simplified the creation of widgets for the PageObject gem using the module shown below. When testing with Watir or Selenium and Cheezy’s PageObject gem, this module allows for the easy creation of test widgets, primarily used for interacting with Gwt or Gxt web applications.
For example, a Gxt table consists of multiple div elements, and each row is itself a table element containing a single row. To interact with the Gxt table, I extend the PageObject::Element::Table class:
class GxtTable < PageObject::Elements::Table
@protected
def child_xpath
".//descendant::tr"
end
end
I then register the class with the PageObject gem. The register_widget method accepts a tag which will be used as the accessor, the class which will be added to the Elements module, and a html element tag which will be used as the top-level html element of the widget, or the html element used in the watir or selenium search.
Widgets.register_widget :gxt_table, GxtTable, :div
The GxtTable then behaves as if it were an html element for the purpose of page-object definitions.
class WidgetTestPageObject
include PageObject
gxt_table(:a_table, :id => "top_div_id")
gxt_table :gxt_block_table do |element|
"block_gxt_table"
end
div(:outer_div)
gxt_table(:a_nested_gxt_table) {|page| page.outer_div_element.gxt_table_element}
end
The code for the WidgetModule is shown below. My next step is to submit this code as a pull request within PageObject.
require 'page-object/elements'
require 'page-object/platforms/selenium_webdriver/page_object'
require 'page-object/platforms/watir_webdriver/page_object'
module Widgets
def self.register_widget(widget_tag, widget_class, base_element_tag)
if widget_class.ancestors.include? PageObject::Elements::Element
define_accessors(PageObject::Accessors, widget_tag)
define_nested_elements(PageObject::Elements::Element, widget_tag)
define_locators(PageObject, widget_tag)
define_selenium_accessors(PageObject::Platforms::SeleniumWebDriver::PageObject, widget_tag, widget_class, base_element_tag)
define_watir_accessors(PageObject::Platforms::WatirWebDriver::PageObject, widget_tag, widget_class, base_element_tag)
end
end
@private
def self.define_accessors(base, widget_tag)
accessors_module = Module.new do
class_eval "def #{widget_tag}(name, identifier={}, &block)
identifier={:index=>0} if identifier.empty?
define_method(\"\#{name}_element\") do
return call_block(&block) if block_given?
platform.#{widget_tag}_for(identifier.clone)
end
define_method(\"\#{name}?\") do
return call_block(&block).exists? if block_given?
platform.#{widget_tag}_for(identifier.clone).exists?
end
end"
end
base.send(:include, accessors_module)
end
def self.define_watir_accessors(base, widget_tag, widget_class, base_element_tag)
base.send(:define_method, "#{widget_tag}_for") do |identifier|
find_watir_element("#{base_element_tag}(identifier)", widget_class, identifier, base_element_tag)
end
base.send(:define_method, "#{widget_tag}s_for") do |identifier|
find_watir_elements("#{base_element_tag}(identifier)", widget_class, identifier, base_element_tag)
end
end
def self.define_selenium_accessors(base, widget_tag, widget_class, base_element_tag)
base.send(:define_method, "#{widget_tag}_for") do |identifier|
find_selenium_element(identifier, widget_class, base_element_tag)
end
base.send(:define_method, "#{widget_tag}s_for") do |identifier|
find_selenium_elements(identifier, widget_class, base_element_tag)
end
end
def self.define_nested_elements(base, widget_tag)
base.send(:define_method, "#{widget_tag}_element") do |identifier={}|
identifier={:index => 0} if identifier.empty?
@platform.send("#{widget_tag}_for", identifier.clone)
end
base.send(:define_method, "#{widget_tag}_elements") do |identifier={}|
identifier={:index => 0} if identifier.empty?
@platform.send("#{widget_tag}s_for", identifier.clone)
end
end
def self.define_locators(base, widget_tag)
base.send(:define_method, "#{widget_tag}_element") do |identifier={}|
identifier={:index => 0} if identifier.empty?
platform.send("#{widget_tag}_for", identifier.clone)
end
base.send(:define_method, "#{widget_tag}_elements") do |identifier={}|
platform.send("#{widget_tag}s_for", identifier.clone)
end
end
end
In adding some user interface tests to a .NET windows application, I stumbled upon a custom TabControl whose children were not accessible to the Microsoft CodedUI libraries. Inspect.exe (a tool for browsing the control tree of windows application) showed TabItems without content.
I was able to restore testability to the application with a minor change to the custom TabControl, and by implementing custom AutomationPeers. The complete source code is shown below, but first, a look at how the problem was created, and an explanation of the solution.
The problematic TabControl came from this post on StackOverflow: Stop TabControl From Recreating It’s Children. The TabControlEx class prevents the TabControl form unloading it’s children from memory when the user switches between TabItems. To accomplish this, the TabControl has been changed from a type of ContentPresenter to a type of Panel. The standard TabControl copies the TabItem content into it’s own SelectedContent dependency property, which then makes the controls within the TabItem visible and accessible. TabControl is a ContentPresenters whose children are replaced when the user switches between tabs. TabControlEx instead adds TabItems to the ‘Children’ collection of the Panel. When the user switches between tabs, the selected tab is made visible, and the other tabs are collapsed.
The TabItemAutomationPeer which provides automation accessibility for the contents of TabItems loads it’s child controls by referring to the TabControl. Specifically, GetChildrenCore method casts the parent of the TabItem as a TabControl, and returns the children of the TabControl.SelectedContentPresenter property. The SelectedContentPresenter property calls GetTemplateChild to return the current state of the TabControl ContentPresenter using a hard coded TemplatePart Name. When the TabItemAutomationPeer attempts to return its children from the TabControlEx, none are found. As a result, the TabItems are visible within the control tree, but they do not appear to have any contents.
In order to return testability to this control class, I have added a new SelectedContentPresenter method which uses methods already provided on the class to return the ContentPresenter for the selected TabItem. This method replicates the interaction between the TabControl and the TabItemAutomationPeer, making use of the collection of ContentPresenters contained in the TabControlEx.
I have also overridden the TabControlAutomationPeer, and the TabItemAutomationPeer classes. The TabItemAutomationPeer was only modified to refer to the TabControlEx class and use the SelectedContentPresenter method on that class. The TabControlAutomationPeer was only modified to make use of the CustomTabItemAutomationPeer class, and the TabControlEx class was also modified to load the CustomTabControlAutomationPeer by overriding the OnCreateAutomationPeer event.
// Extended TabControl which saves the displayed item so you don't get the performance hit of
// unloading and reloading the VisualTree when switching tabs
// Obtained from http://eric.burke.name/dotnetmania/2009/04/26/22.09.28
// and made a some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations
[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : System.Windows.Controls.TabControl
{
// Holds all items, but only marks the current tab's item as visible
private Panel _itemsHolder = null;
// Temporaily holds deleted item in case this was a drag/drop operation
private object _deletedObject = null;
public TabControlEx()
: base()
{
// this is necessary so that we get the initial databound selected item
this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
/// <summary>
/// Overrides OnCreateAutomationPeer to use CustomTabControlAutomationPeer..
/// </summary>
protected override AutomationPeer OnCreateAutomationPeer()
{
return new CustomTabControlAutomationPeer(this);
}
/// <summary>
/// if containers are done, generate the selected item
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
UpdateSelectedItem();
}
}
/// <summary>
/// get the ItemsHolder and generate any children
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel;
UpdateSelectedItem();
}
/// <summary>
/// when the items change we remove any generated panel children and add any new ones as necessary
/// </summary>
/// <param name="e"></param>
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (_itemsHolder == null)
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Reset:
_itemsHolder.Children.Clear();
if (base.Items.Count > 0)
{
base.SelectedItem = base.Items[0];
UpdateSelectedItem();
}
break;
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
// Search for recently deleted items caused by a Drag/Drop operation
if (e.NewItems != null && _deletedObject != null)
{
foreach (var item in e.NewItems)
{
if (_deletedObject == item)
{
// If the new item is the same as the recently deleted one (i.e. a drag/drop event)
// then cancel the deletion and reuse the ContentPresenter so it doesn't have to be
// redrawn. We do need to link the presenter to the new item though (using the Tag)
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
int index = _itemsHolder.Children.IndexOf(cp);
(_itemsHolder.Children[index] as ContentPresenter).Tag =
(item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
}
_deletedObject = null;
}
}
}
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
_deletedObject = item;
// We want to run this at a slightly later priority in case this
// is a drag/drop operation so that we can reuse the template
this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
new Action(delegate()
{
if (_deletedObject != null)
{
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
this._itemsHolder.Children.Remove(cp);
}
}
}
));
}
}
UpdateSelectedItem();
break;
case NotifyCollectionChangedAction.Replace:
throw new NotImplementedException("Replace not implemented yet");
}
}
/// <summary>
/// update the visible child in the ItemsHolder
/// </summary>
/// <param name="e"></param>
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
/// <summary>
/// generate a ContentPresenter for the selected item
/// </summary>
void UpdateSelectedItem()
{
if (_itemsHolder == null)
{
return;
}
// generate a ContentPresenter if necessary
TabItem item = GetSelectedTabItem();
if (item != null)
{
CreateChildContentPresenter(item);
}
// show the right child
foreach (ContentPresenter child in _itemsHolder.Children)
{
child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
}
}
/// <summary>
/// create the child ContentPresenter for the given item (could be data or a TabItem)
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
ContentPresenter CreateChildContentPresenter(object item)
{
if (item == null)
{
return null;
}
ContentPresenter cp = FindChildContentPresenter(item);
if (cp != null)
{
return cp;
}
// the actual child to be added. cp.Tag is a reference to the TabItem
cp = new ContentPresenter();
cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
cp.ContentTemplate = this.SelectedContentTemplate;
cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
cp.ContentStringFormat = this.SelectedContentStringFormat;
cp.Visibility = Visibility.Collapsed;
cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
_itemsHolder.Children.Add(cp);
return cp;
}
/// <summary>
/// Provides the content Presenter to the TabItem for UI automation.
/// </summary>
/// <returns>ContentPresenter</returns>
internal ContentPresenter SelectedContentPresenter
{
get
{
return FindChildContentPresenter(GetSelectedTabItem());
}
}
/// <summary>
/// Find the CP for the given object. data could be a TabItem or a piece of data
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
ContentPresenter FindChildContentPresenter(object data)
{
if (data is TabItem)
{
data = (data as TabItem).Content;
}
if (data == null)
{
return null;
}
if (_itemsHolder == null)
{
return null;
}
foreach (ContentPresenter cp in _itemsHolder.Children)
{
if (cp.Content == data)
{
return cp;
}
}
return null;
}
/// <summary>
/// copied from TabControl; wish it were protected in that class instead of private
/// </summary>
/// <returns></returns>
protected TabItem GetSelectedTabItem()
{
object selectedItem = base.SelectedItem;
if (selectedItem == null)
{
return null;
}
if (_deletedObject == selectedItem)
{
}
TabItem item = selectedItem as TabItem;
if (item == null)
{
item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
}
return item;
}
}
/// <summary>
/// Extends the TabControlAutomationPeer to use the CustomItemAutomationPeer
/// </summary>
public class CustomTabControlAutomationPeer : TabControlAutomationPeer
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomTabControlAutomationPeer"/> class.
/// </summary>
public CustomTabControlAutomationPeer(TabControl owner)
: base(owner)
{
}
/// <summary>
/// overrides GetClasNamecore to display TabControlEx.
/// </summary>
protected override string GetClassNameCore()
{
return "TabControlEx";
}
/// <summary>
/// overrides CreateItemAutomationPeer to returnCustomTabItemAutomationPeer
/// </summary>
/// <returns>ItemAutomationPeer</returns>
protected override ItemAutomationPeer CreateItemAutomationPeer(object item)
{
return new CustomTabItemAutomationPeer(item, this);
}
}
/// <summary>
/// Extends the TabItemAutomation peer to add child Automation peers from the TabControlEx
/// </summary>
public class CustomTabItemAutomationPeer :TabItemAutomationPeer
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomTabItemAutomationPeer"/> class.
/// </summary>
public CustomTabItemAutomationPeer(object owner, TabControlAutomationPeer tabControlAutomationPeer)
: base(owner, tabControlAutomationPeer)
{
}
/// <summary>
/// overrides GetChildrenCore to return return children from the content presenters in a TabControlEx.
/// </summary>
protected override List<AutomationPeer> GetChildrenCore()
{
List<AutomationPeer> childrenCore = base.GetChildrenCore();
TabControlEx owner = base.ItemsControlAutomationPeer.Owner as TabControlEx;
if (owner == null)
{
return childrenCore;
}
ContentPresenter selectedContentPresenter = owner.SelectedContentPresenter;
if (selectedContentPresenter == null)
{
return childrenCore;
}
List<AutomationPeer> children = new FrameworkElementAutomationPeer(selectedContentPresenter).GetChildren();
if (children == null)
{
return childrenCore;
}
if (childrenCore == null)
{
return children;
}
childrenCore.AddRange(children);
return childrenCore;
}
}