bridgetfoote
Topic Author
Posts: 6
Joined: 10 May 2023, 18:52

Custom ItemsControl to maintain focused index

16 Nov 2023, 17:11

Hello, trying to create a custom items control that maintains the index of focus when an item is replaced. For our use case we have an ItemsControl that contains a number of content cards. While we are retrieving the information to create the proper cards in the ItemsControl, it is initialized with a list of loading cards which are then replaced by the proper cards. We want to maintain the index of focus once the proper cards load in - ex: if the second loading card is in focus, when it is replaced that replacement card which is still the second card in the items control is focused. All cards inherit from the same view model class, so we have SpecificCardTypeOneViewModel, SpecificCardTypeTwoViewModel, etc. which all inherit from BaseCardViewModel. The ItemsSource for the ItemsControl is an observable collection of BaseCardViewModel.

Attempted to achieve this by overloading OnItemsChanged and OnPreviewGotKeyboardFocus as follows:
void CustomItemsControl::OnItemsChanged(const Noesis::NotifyCollectionChangedEventArgs& args)
{
    switch(args.action)
    {
        case Noesis::NotifyCollectionChangedAction_Replace:
        {
            m_replacingElement = true;
            if (m_focusedIndex == args.newIndex)
            {
                Noesis::ItemCollection* items = GetItems();
                Noesis::ItemContainerGenerator* generator = GetItemContainerGenerator();
                int index = args.newIndex;

                OnBringItemIntoView(items->GetItemAt(index));
                Noesis::UIElement* element = Noesis::DynamicCast<Noesis::UIElement*>(generator->ContainerFromIndex(index));
                if (element != 0)
                {
                    if (element->Focus() || element->GetIsKeyboardFocusWithin())
                    {
                        return;
                    }

                    // container is not focusable, try to focus its contents
                    Noesis::TraversalRequest request = { Noesis::FocusNavigationDirection_First, false };
                    element->MoveFocus(request);
                }
            }
        }
    }
}

Noesis::Vector<Noesis::UIElement*> CustomItemsControl::FindFocusableElements(Noesis::Visual* root)
{
    Noesis::Vector<Noesis::UIElement*> children;

    if (root == nullptr)
    {
        return children;
    }

	const uint32_t visualChildCount = Noesis::VisualTreeHelper::GetChildrenCount(root);
	for (uint32_t n = 0; n < visualChildCount; n++) {
		Noesis::Visual* child = Noesis::VisualTreeHelper::GetChild(root, n);
		Noesis::UIElement* element = Noesis::DynamicCast<Noesis::UIElement*>(child);

		// If not enabled or visible, don't even bother with it
		if (!element->GetIsEnabled() || !element->GetIsVisible())
			continue;

		// If it's focusable, that's what we want
		if (element->GetFocusable()) {
            children.PushBack(element);
		}
		// Otherwise, keep searching downward
		else {
			auto focusableChildren = FindFocusableElements(child);
			children.Insert(children.End(), focusableChildren.Begin(), focusableChildren.End());
		}
    }

    return children;
}

void CustomItemsControl::OnPreviewGotKeyboardFocus(const Noesis::KeyboardFocusChangedEventArgs& args)
{
    if (!m_replacingElement)
    {
        auto focusableElements = FindFocusableElements(this);
        int numFocusableElements = focusableElements.Size();
        for (int i = (numFocusableElements - 1); i >= 0; i--)
        {
            if (focusableElements.Back() == args.newFocus)
            {
                m_focusedIndex = i;
                return;
            }
            focusableElements.PopBack();
        }
    }
    else 
    {
        m_replacingElement = false;
    }
}
However, this only works when the card that is being replaced is the same subclass as the card that is replacing it, so they both are type SpecificCardTypeOneViewModel. If we try to replace a SpecificCardTypeOneViewModel item with a SpecificCardTypeTwoViewModel we get into the OnItemsChanged override and element->Focus() returns false but element->GetIsKeyboardFocusWithin() returns true and so we return but the ItemsControl loses focus.

Is there something different happening to the container in this case? What could be causing it to not focus correctly?
 
User avatar
sfernandez
Site Admin
Posts: 3008
Joined: 22 Dec 2011, 19:20

Re: Custom ItemsControl to maintain focused index

17 Nov 2023, 19:27

Hi,

Just a suggestion, for finding the index of the item containing the focused element you can just use the ItemsControl::ContainerFromElement() + ItemContainerGenerator::ContainerFromIndex().

Regarding your problem, when the item being replaced has the same type, what happens with the check
if (element->Focus() || element->GetIsKeyboardFocusWithin())
Is also element->Focus() returning false, and element->GetIsKeyboardFocusWithin() returning true?

Could you try to call element->UpdateLayout() before calling element->Focus()? It might happen that the new type requires the use of a new data template and elements inside are not yet generated.

Who is online

Users browsing this forum: Ahrefs [Bot] and 8 guests