Page 1 of 1

Help setting focus to element in ItemsControl

Posted: 10 Feb 2025, 18:41
by maxnt
Hi Noesis fokes,

I'm trying to set the default focus to the first element of an ItemControl, on load and if the items change. However, my below code doesn't work.
element->Focus() and element->MoveFocus(request) return false. The end result is no items have focus.

Please advise on what I'm doing wrong.

Thanks in advanced!
MAUserControl::MAUserControl() : UserControl()
{
    Loaded() += Noesis::MakeDelegate(this, &MAUserControl::OnLoaded);
}

void MAUserControl::OnLoaded(Noesis::BaseComponent*, const Noesis::RoutedEventArgs&)
{
    const char* focus_name = GetValue<Noesis::String>(DefaultFocusElementNameProperty).Str();

    if (focus_name != nullptr)
    {
        Noesis::BaseComponent* focus_child = FindName(focus_name);

        if (focus_child != nullptr)
        {
            Noesis::ItemsControl* item_control = Noesis::DynamicCast<Noesis::ItemsControl*>(focus_child);

            if (item_control != nullptr)
            {
                Noesis::ItemCollection* items = item_control->GetItems();
                if (items != nullptr)
                {
                    items->CollectionChanged() += Noesis::MakeDelegate(this, &MAUserControl::OnItemsChaged);
                }
            }
        }
    }

    SetDefaultFocus();
}

void MAUserControl::OnItemsChaged(Noesis::BaseComponent*, const Noesis::NotifyCollectionChangedEventArgs&)
{
    SetDefaultFocus();
}

void MAUserControl::SetDefaultFocus()
{
    const char* focus_name = GetValue<Noesis::String>(DefaultFocusElementNameProperty).Str();

    if (focus_name != nullptr)
    {
        Noesis::BaseComponent* focus_child = FindName(focus_name);

        if (focus_child != nullptr)
        {

            Noesis::ItemsControl* item_control = Noesis::DynamicCast<Noesis::ItemsControl*>(focus_child);

            if (item_control != nullptr && item_control->GetHasItems())
            {
                Noesis::ItemContainerGenerator* generator = item_control->GetItemContainerGenerator();
                Noesis::UIElement* element = Noesis::DynamicCast<Noesis::UIElement*>(generator->ContainerFromIndex(0));

                if (element != nullptr)
                {
                    if (element->Focus() || element->GetIsKeyboardFocusWithin())
                    {
                        return;
                    }

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

                    Noesis::DependencyObject* focusScope = Noesis::FocusManager::GetFocusScope(element);
                    if (focusScope != nullptr && focusScope != element)
                    {
                        Noesis::FocusManager::SetFocusedElement(focusScope, element);
                        return;
                    }
                }
            }
        }
    }
}
<ItemsControl.ItemTemplate>
    <DataTemplate>
        <Button Height="50" Margin="0 0 0 5"
                DockPanel.Dock="Bottom"
                Style="{StaticResource ButtonStyleRectangular_Resize}"
                Content="{Binding PlayerChoiceText}">
        </Button>
    </DataTemplate>
</ItemsControl.ItemTemplate>
Update: I have found that the above works if I add a very small timer to set the focus after a few ms. Any thought on how I can get round this hack would be great :D

Re: Help setting focus to element in ItemsControl

Posted: 13 Feb 2025, 23:49
by maxnt
I have found the solution, posting my findings for others :)

ItemControls go through a few stages before its children can accept a focus. Turns out the OnItemsChanged changed event is purely a data change and is too early, the visual containers have not been built yet.

A rough order of execution when changing the ItemsControl Items Source

ItemsControl->OnItemsChanged
ItemsControl->ContentGenerator->OnItemsChanged
ItemsControl->ContentGenerator->BatchGeneration
ItemsControl->ContentGenerator->OnStatusChanged:GeneratorStatus_GeneratingContainers
ItemsControl->ContentGenerator->OnStatusChanged:GeneratorStatus_ContainersGenerated
ItemsControl->ContentGenerator->ItemContainer->OnInitialised
ItemsControl->ContentGenerator->ItemContainer->OnLoaded <--- at this point I found I can reliably set focus

Here is a snippet of my custom focus Dependency object
void MAFocusDependencyObject::FocusOnItemChange(Noesis::FrameworkElement* element, bool attach)
{
    Noesis::ItemsControl* item_control = Noesis::DynamicCast<Noesis::ItemsControl*>(element);
    if (item_control != nullptr)
    {
        Noesis::ItemContainerGenerator* generator = item_control->GetItemContainerGenerator();
        generator->StatusChanged() -= Noesis::MakeDelegate(this, &MAFocusDependencyObject::OnCollectionGenStatusChanged);

        if (attach)
        {
            generator->StatusChanged() += Noesis::MakeDelegate(this, &MAFocusDependencyObject::OnCollectionGenStatusChanged);
        }
    }
}

void MAFocusDependencyObject::OnCollectionGenStatusChanged(Noesis::BaseComponent* component, const Noesis::EventArgs&)
{
    Noesis::ItemContainerGenerator* generator = Noesis::DynamicCast <Noesis::ItemContainerGenerator*>(component);
    if (generator->GetStatus() == Noesis::GeneratorStatus::GeneratorStatus_ContainersGenerated)
    {
        Noesis::FrameworkElement* item_focus_child = Noesis::DynamicCast<Noesis::FrameworkElement*>(generator->ContainerFromIndex(0));
        if (item_focus_child != nullptr)
        {
            item_focus_child->Loaded() -= Noesis::MakeDelegate(this, &MAFocusDependencyObject::OnCollectionItemLoaded);
            item_focus_child->Loaded() += Noesis::MakeDelegate(this, &MAFocusDependencyObject::OnCollectionItemLoaded);
        }
    }
}

void MAFocusDependencyObject::OnCollectionItemLoaded(Noesis::BaseComponent* component, const Noesis::RoutedEventArgs&)
{
    Noesis::FrameworkElement* focus_element = Noesis::DynamicCast<Noesis::FrameworkElement*>(component);
    if (focus_element != nullptr)
    {
        SetFocus(focus_element);
    }
}

Re: Help setting focus to element in ItemsControl

Posted: 17 Feb 2025, 19:13
by sfernandez
Thanks for sharing your solution.

As you described, a UI element must be connected to the View and visible/enabled to receive focus. Waiting to the Loaded event is a good place for this to be true.

Another option would be to hook to the LayoutUpdated event instead when the ItemsSource changed.