a.lashkevich
Topic Author
Posts: 11
Joined: 23 Sep 2024, 08:50

ContentPresenter DataContext bug

13 Dec 2024, 14:11

hi noesis team, please help me with my problem

I want to create custom control like main screen on android/iphone device. So I've created grid with several pages and inside each page I've placed several buttons. Also I've created custom component Swiper to swipe pages like on main screen of android or ios system.
So my hierarcy is: SWIPER (collection of pages) -> ItemsControl (one page, collection of buttons) -> Button ( one simple button with icon and text).
And I've created such view models:
using Page = Noesis::ObservableCollection<TabletGridButtonVM>;
Noesis::Ptr<ObservableCollection<Page>> pages;
    <br:Swiper 
              Margin="0,0,0,0"
              HorizontalAlignment="Center"
              VerticalAlignment="Center"
              Grid.Row="1" PagesSource="{Binding Pages}">
          <br:Swiper.PageDataTemplate>    
            <DataTemplate>
              <Grid x:Name="PageGrid" DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=UserControl}}">
                
                <ItemsControl Margin="40,40,40,40" Background="#00000000" ItemsSource="{Binding}" >
                  <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                      <UniformGrid Columns="5" Rows="3"/>
                    </ItemsPanelTemplate>
                  </ItemsControl.ItemsPanel> 
                  <ItemsControl.ItemTemplate>
                    <DataTemplate>
                      <Grid >
                      <TextBlock Text="{Binding Text}" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,152,0,0" Foreground="#FFFFFFFF"/>
                      <Button Background="#00000000" Foreground="#00141414" BorderBrush="#00000000">
                        <Grid Width="200" Height="200" HorizontalAlignment="Center" VerticalAlignment="Center" Background="#00000000">
                          <Image Source="{Binding Icon}" Width="116" Height="116" Margin="0,0,0,32"/>
                          <TextBlock Text="{Binding Text}" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,152,0,0" Foreground="#FFFFFFFF"/>
                          <Rectangle Visibility="{Binding HasBadge, Converter={BooleanToVisibilityConverter}}" RadiusX="11" RadiusY="11" VerticalAlignment="Center" HorizontalAlignment="Center" Height="24" Width="24" Fill="#E2310C" Margin="0,-132,-104,0"/>
                        </Grid>
                      </Button>
                    </Grid>
                    </DataTemplate>
                  </ItemsControl.ItemTemplate>
                </ItemsControl>
                
              </Grid>
            </DataTemplate>
          </br:Swiper.PageDataTemplate>

    </br:Swiper>
  
also I've added such style for swiper
  
  <!-- Swiper -->
  <DataTemplate x:Key="Template.SwiperDot">
    <Ellipse Height="10" Width="10" Fill="{DynamicResource Brush.SwiperDot}" Margin="7 0 0 0"/>
  </DataTemplate>
  <DataTemplate x:Key="Template.SwiperDot.Selected">
    <Ellipse Height="10" Width="10" Fill="{DynamicResource Brush.SwiperDot.Selected}" Margin="7 0 0 0"/>
  </DataTemplate>
  <ControlTemplate x:Key="Template.Swiper" TargetType="{x:Type br:Swiper}">
    <Grid ClipToBounds="True">
      <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="20"/>
      </Grid.RowDefinitions>
      <ContentPresenter
        Grid.Row="0"
        x:Name="SWIPER_content" DataContext="{TemplateBinding}">
      </ContentPresenter>
      <StackPanel
        x:Name="SWIPER_pagination"
        Grid.Row="1"
        Orientation="Horizontal"
        HorizontalAlignment="Center"
        Margin="0 7 0 0">
      </StackPanel>
    </Grid>
  </ControlTemplate>
  <Style TargetType="{x:Type br:Swiper}">
    <Setter Property="br:Swiper.Template" Value="{DynamicResource Template.Swiper}"/>
    <Setter Property="br:Swiper.DotDataTemplate" Value="{DynamicResource Template.SwiperDot}"/>
    <Setter Property="br:Swiper.CurrentDotDataTemplate" Value="{DynamicResource Template.SwiperDot.Selected}"/>
  </Style>
  
when I open GUI Inspector, I've found that ContentPresenter doesn't have DataContext (check 1st image)
content_presenter_1.jpg
when I'm adding manually binding for DataContext field <Grid x:Name="PageGrid" DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=UserControl}}">, DataContext was passed deeper but in the ContentPresenter in ItemsControl it is also wrong (check 2nd image)
content_presenter_2.jpg
please help me what I'm doing wrong ?
 
User avatar
sfernandez
Site Admin
Posts: 3198
Joined: 22 Dec 2011, 19:20

Re: ContentPresenter DataContext bug

13 Dec 2024, 18:53

Hi,

The ContentPresenter deals with the DataContext in a special way, as it uses the Content property as the DataContext, so the ContentTemplate can bind to properties of the Content.

In the attached xaml, the template for the Swiper control contains a ContentPresenter named "SWIPER_content" that is not setting its Content, so it will set its DataContext to NULL. If you want it to inherit the Swiper control's DataContext you can set the following binding:
<ContentPresenter x:Name="SWIPER_content" Grid.Row="0" Content="{Binding}"/>
Then you are attaching to the SWIPER_content ContentPresenter a UserControl (I guess it could also be a ContentControl as you're not loading any XAML associated to that UserControl), but I don't know if you're setting its Content property to bind the DataContext of the Swiper control. Otherwise its own ContentPresenter will also have a NULL DataContext (because a ContentPresenter in a ContentControl's template binds to the ContentControl.Content property by default).

Please let me know if that helps.
 
a.lashkevich
Topic Author
Posts: 11
Joined: 23 Sep 2024, 08:50

Re: ContentPresenter DataContext bug

16 Dec 2024, 08:17

I'm very sorry, my results was without DataContext="{TemplateBinding}" in Swiper ContentPresenter, I set Content from code, let me attach c++ part of my SWIPER,
in overrided function OnApplyTemplate() I call ShowCurrentPage(), which calls SetContent(...) and SetDataContext(...) for content manually from code
#include "Swiper.xaml.h"

#include <NsCore/Ptr.h>
#include <NsGui/UIElementCollection.h>
#include <NsGui/UIElementData.h>

#include "obfuscate.h"

namespace Gui
{

namespace
{

constexpr float LERP_VISCOSITY = 0.05f;
const char* SWIPER_CONTENT_NAME = AY_OBFUSCATE("SWIPER_content");
const char* SWIPER_PAGINATION_NAME = AY_OBFUSCATE("SWIPER_pagination");

// need to eliminate include math.h
inline float Lerp(float from, float to, float dt, float viscosity)
{
    if (viscosity < 1e-9f)
        return to;
    else
        return from + (1.0f - expf(-dt / viscosity)) * (to - from);
}

};  // namespace

static void OnPagesElementsChanged(Noesis::DependencyObject* object,
                                    const Noesis::DependencyPropertyChangedEventArgs& event)
{
    CSwiper* swiper = Noesis::DynamicCast<CSwiper*>(object);
    if (!swiper)
        return;

    swiper->SetPagesElements(event.NewValue<Noesis::Ptr<SwiperPagesCollection>>());
}

static void OnPagesViewModelsChanged(Noesis::DependencyObject* object,
                                   const Noesis::DependencyPropertyChangedEventArgs& event)
{
    CSwiper* swiper = Noesis::DynamicCast<CSwiper*>(object);
    if (!swiper)
        return;

    swiper->SetPagesViewModels(event.NewValue<Noesis::Ptr<Noesis::BaseComponent>>());
}

static void OnPageTemplateChanged(Noesis::DependencyObject* object,
                                 const Noesis::DependencyPropertyChangedEventArgs& event)
{
    CSwiper* swiper = Noesis::DynamicCast<CSwiper*>(object);
    if (!swiper)
        return;

    swiper->SetPageDataTemplate(event.NewValue<Noesis::Ptr<Noesis::DataTemplate>>());
}

static void OnDotTemplateChanged(Noesis::DependencyObject* object,
                                 const Noesis::DependencyPropertyChangedEventArgs& event)
{
    CSwiper* swiper = Noesis::DynamicCast<CSwiper*>(object);
    if (!swiper)
        return;

    swiper->SetDotDataTemplate(event.NewValue<Noesis::Ptr<Noesis::DataTemplate>>());
}

static void OnCurrentDotTemplateChanged(Noesis::DependencyObject* object,
                                        const Noesis::DependencyPropertyChangedEventArgs& event)
{
    CSwiper* swiper = Noesis::DynamicCast<CSwiper*>(object);
    if (!swiper)
        return;

    swiper->SetCurrentDotDataTemplate(event.NewValue<Noesis::Ptr<Noesis::DataTemplate>>());
}

CSwiper::CSwiper() : m_contentPresenterTransform(Noesis::MakePtr<Noesis::TranslateTransform>()) {}

uint32_t CSwiper::GetPagesCount() const
{
    if (m_pagesElements)
        return m_pagesElements->Count();

    if (m_pagesViewModels)
        return m_pagesViewModels->Count();

    return 0;
}

void CSwiper::SetPagesElements(Noesis::Ptr<SwiperPagesCollection> pagesElements)
{
    m_pagesElements = pagesElements;
}

void CSwiper::SetPagesViewModels(Noesis::Ptr<Noesis::BaseComponent> pagesViewModels)
{
    m_pagesViewModels = Noesis::DynamicPtrCast<Noesis::BaseCollection>(pagesViewModels);
}

void CSwiper::SetPageDataTemplate(Noesis::Ptr<Noesis::DataTemplate> dataTemplate)
{
    m_pageDataTemplate = dataTemplate;
}

void CSwiper::SetDotDataTemplate(Noesis::Ptr<Noesis::DataTemplate> dataTemplate)
{
    m_dotDataTemplate = dataTemplate;
}

void CSwiper::SetCurrentDotDataTemplate(Noesis::Ptr<Noesis::DataTemplate> dataTemplate)
{
    m_currentDotDataTemplate = dataTemplate;
}

void CSwiper::OnApplyTemplate()
{
    m_contentPresenter = GetTemplateChild<Noesis::ContentPresenter>(SWIPER_CONTENT_NAME);
    if (!m_contentPresenter)
        return;

    m_contentPresenter->SetRenderTransform(m_contentPresenterTransform);

    // Optional
    m_dotsPagination = GetTemplateChild<Noesis::StackPanel>(SWIPER_PAGINATION_NAME);

    CreatePagination();
    ShowCurrentPage();
}

#ifndef NOESIS_LSP_SERVER
void CSwiper::OnUpdate(double deltaTime)
{
    if (m_bIsCaptured)
        return;

    float moveTo = 0.0f;
    float width = GetActualWidth();
    float currentXPos = m_contentPresenterTransform->GetX();

    switch (m_state)
    {
        case EState::MOVE_TO_LEFT_BACKSTAGE:
            if (currentXPos < -width + width * 0.1f)
            {
                m_state = EState::MOVE_TO_CENTER;
                currentXPos = width;
                m_previousPageIndex = m_currentPageIndex;
                ++m_currentPageIndex;
                UpdatePagination();
                ShowCurrentPage();
                break;
            }

            moveTo = -width;
            break;

        case EState::MOVE_TO_RIGHT_BACKSTAGE:
            if (currentXPos > width - width * 0.1f)
            {
                m_state = EState::MOVE_TO_CENTER;
                currentXPos = -width;
                m_previousPageIndex = m_currentPageIndex;
                --m_currentPageIndex;
                UpdatePagination();
                ShowCurrentPage();
                break;
            }

            moveTo = width;
            break;
    }

    m_contentPresenterTransform->SetX(Lerp(currentXPos, moveTo, deltaTime, LERP_VISCOSITY));
}
#endif

void CSwiper::OnMouseDown(const Noesis::MouseButtonEventArgs& e)
{
    OnManipulatorDownImplementation(e.position.x);
    e.handled = true;
}

void CSwiper::OnMouseUp(const Noesis::MouseButtonEventArgs& e)
{
    OnManipulatorUpImplementation(e.position.x);
    e.handled = true;
}

void CSwiper::OnMouseMove(const Noesis::MouseEventArgs& e)
{
    OnManipulatorMoveImplementation(e.position.x);
    e.handled = true;
}

void CSwiper::OnMouseLeave(const Noesis::MouseEventArgs& e)
{
    OnManipulatorUpImplementation(e.position.x);
}

void CSwiper::OnTouchDown(const Noesis::TouchEventArgs& e)
{
    OnManipulatorDownImplementation(e.touchPoint.x);
    e.handled = true;
}

void CSwiper::OnTouchUp(const Noesis::TouchEventArgs& e)
{
    OnManipulatorUpImplementation(e.touchPoint.x);
    e.handled = true;
}

void CSwiper::OnTouchMove(const Noesis::TouchEventArgs& e)
{
    OnManipulatorMoveImplementation(e.touchPoint.x);
    e.handled = true;
}

void CSwiper::OnTouchLeave(const Noesis::TouchEventArgs& e)
{
    OnManipulatorUpImplementation(e.touchPoint.x);
}

void CSwiper::OnManipulatorMoveImplementation(const float xPos)
{
    if (!m_bIsCaptured)
        return;

    const float deltaX = -m_capturedPositionX + xPos;
    m_contentPresenterTransform->SetX(deltaX);
}

void CSwiper::OnManipulatorUpImplementation(const float xPos)
{
    if (!m_bIsCaptured)
        return;

    const float deltaX = -m_capturedPositionX + xPos;
    const float width = GetActualWidth();

    if (deltaX < -0.5f * width)
    {
        bool isLastPage = m_currentPageIndex == GetPagesCount() - 1;
        if (!isLastPage)
            m_state = EState::MOVE_TO_LEFT_BACKSTAGE;
    }

    else if (deltaX > 0.5 * width)
    {
        bool isFirstPage = m_currentPageIndex == 0;
        if (!isFirstPage)
            m_state = EState::MOVE_TO_RIGHT_BACKSTAGE;
    }

    m_bIsCaptured = false;
}

void CSwiper::OnManipulatorDownImplementation(const float xPos)
{
    if (m_bIsCaptured)
        return;

    m_capturedPositionX = xPos - m_contentPresenterTransform->GetX();
    m_bIsCaptured = true;
}

void CSwiper::ShowCurrentPage()
{
    if (m_pagesElements)
    {
        if (m_currentPageIndex >= m_pagesElements->Count())
            return;

        SetContent(m_pagesElements->Get(m_currentPageIndex));
    }
    else if (m_pagesViewModels)
    {
        if (m_currentPageIndex >= m_pagesViewModels->Count())
            return;

        if (!m_pageViewModelControl)
        {
            m_pageViewModelControl = Noesis::MakePtr<Noesis::UserControl>();
            if (m_pageDataTemplate)
                m_pageViewModelControl->SetContentTemplate(m_pageDataTemplate);
        }

        SetContent(m_pageViewModelControl);
        m_pageViewModelControl->SetDataContext(m_pagesViewModels->GetComponent(m_currentPageIndex).GetPtr());
    }
}

void CSwiper::CreatePagination()
{
    if (!m_dotsPagination)
        return;

    m_dotsPagination->GetChildren()->Clear();

    for (uint32_t i = 0; i < GetPagesCount(); ++i)
    {
        Noesis::Ptr<Noesis::UserControl> dotControl = Noesis::MakePtr<Noesis::UserControl>();

        const bool isActiveDot = i == m_currentPageIndex;
        Noesis::DataTemplate* pDataTemplate = isActiveDot ? m_currentDotDataTemplate : m_dotDataTemplate;
        dotControl->SetContentTemplate(pDataTemplate);

        m_dotsPagination->GetChildren()->Add(dotControl);
    }
}

void CSwiper::UpdatePagination()
{
    if (!m_dotsPagination)
        return;

    Noesis::UserControl* currentActiveDotControl =
        static_cast<Noesis::UserControl*>(m_dotsPagination->GetChildren()->Get(m_currentPageIndex));
    currentActiveDotControl->SetContentTemplate(m_currentDotDataTemplate);
    currentActiveDotControl->ApplyTemplate();

    Noesis::UserControl* previousActiveDotControl =
        static_cast<Noesis::UserControl*>(m_dotsPagination->GetChildren()->Get(m_previousPageIndex));
    previousActiveDotControl->SetContentTemplate(m_dotDataTemplate);
    previousActiveDotControl->ApplyTemplate();
}

NS_BEGIN_COLD_REGION

NS_IMPLEMENT_REFLECTION_(SwiperPagesCollection, "SwiperPagesCollection")

NS_IMPLEMENT_REFLECTION(CSwiper, "Swiper")
{
    Noesis::UIElementData* data = NsMeta<Noesis::UIElementData>(Noesis::TypeOf<SelfClass>());

    data->RegisterProperty<Noesis::Ptr<SwiperPagesCollection>>(
        s_pagesProperty,
        "Pages",
        Noesis::PropertyMetadata::Create(
            Noesis::Ptr<SwiperPagesCollection>(), Noesis::PropertyChangedCallback(OnPagesElementsChanged)));

    data->RegisterProperty<Noesis::Ptr<Noesis::BaseComponent>>(
        s_pagesSourceProperty,
        "PagesSource",
        Noesis::PropertyMetadata::Create(
            Noesis::Ptr<Noesis::BaseComponent>(), Noesis::PropertyChangedCallback(OnPagesViewModelsChanged)));

    data->RegisterProperty<Noesis::Ptr<Noesis::DataTemplate>>(
        s_pageDataTemplateProperty,
        "PageDataTemplate",
        Noesis::PropertyMetadata::Create(
            Noesis::Ptr<Noesis::DataTemplate>(), Noesis::PropertyChangedCallback(OnPageTemplateChanged)));

    data->RegisterProperty<Noesis::Ptr<Noesis::DataTemplate>>(
        s_dotDataTemplateProperty,
        "DotDataTemplate",
        Noesis::PropertyMetadata::Create(
            Noesis::Ptr<Noesis::DataTemplate>(), Noesis::PropertyChangedCallback(OnDotTemplateChanged)));

    data->RegisterProperty<Noesis::Ptr<Noesis::DataTemplate>>(
        s_currentDotDataTemplateProperty,
        "CurrentDotDataTemplate",
        Noesis::PropertyMetadata::Create(
            Noesis::Ptr<Noesis::DataTemplate>(), Noesis::PropertyChangedCallback(OnCurrentDotTemplateChanged)));
}

NS_END_COLD_REGION

const Noesis::DependencyProperty* CSwiper::s_pagesProperty = nullptr;
const Noesis::DependencyProperty* CSwiper::s_pagesSourceProperty = nullptr;
const Noesis::DependencyProperty* CSwiper::s_pageDataTemplateProperty = nullptr;
const Noesis::DependencyProperty* CSwiper::s_dotDataTemplateProperty = nullptr;
const Noesis::DependencyProperty* CSwiper::s_currentDotDataTemplateProperty = nullptr;

}  // namespace Gui
and also your explanation doesn't get answer about empty DataContext in ContentPresenter inside ItemsControl (see 2nd screenshot)
 
User avatar
sfernandez
Site Admin
Posts: 3198
Joined: 22 Dec 2011, 19:20

Re: ContentPresenter DataContext bug

16 Dec 2024, 12:18

I don't see in the attached code that m_contentPresenter Content is set, only the StackPanel is filled with pages in CreatePagination(). And as I mentioned before, you could create a simple ContentControl instead of UserControl there. You're not setting the dotControl Content either, so the data context won't be inherited in the template internal ContentPresenter down the visual tree.

Shouldn't you do something like this?
Noesis::Ptr<Noesis::ContentControl> dotControl = Noesis::MakePtr<Noesis::ContentControl>();
dotControl->SetContent(m_pagesElements->Get(i));
...
 
a.lashkevich
Topic Author
Posts: 11
Joined: 23 Sep 2024, 08:50

Re: ContentPresenter DataContext bug

16 Dec 2024, 13:32

m_contentPresenter has correct content and DataContext, I have a problem with auto-generated ContentPresenter which is deeper on 2 levels (please see new screenshot), also I've replaced creating UserControl to creating ContentControl, still no effect
content_presenter_3.jpg
 
User avatar
sfernandez
Site Admin
Posts: 3198
Joined: 22 Dec 2011, 19:20

Re: ContentPresenter DataContext bug

17 Dec 2024, 10:58

The auto-generated ContentPresenter sets its DataContext from the Content of its templated parent (which is null as shown in your screenshot). You have to set the Content property of m_pageViewModelControl to the desired data so it is inherited down the tree of the ContentPresenter:
m_pageViewModelControl->SetContent(pageViewModel);
Could you try that?
 
a.lashkevich
Topic Author
Posts: 11
Joined: 23 Sep 2024, 08:50

Re: ContentPresenter DataContext bug

17 Dec 2024, 13:05

yes, it helps, thanks a lot,
but I'm still have a second problem, with incorrect DataContext in ContentPresenter inside UniformGrid inside ItemsControl (see 2nd screenshot)
 
a.lashkevich
Topic Author
Posts: 11
Joined: 23 Sep 2024, 08:50

Re: ContentPresenter DataContext bug

20 Dec 2024, 15:59

@sfernandez please help
 
User avatar
sfernandez
Site Admin
Posts: 3198
Joined: 22 Dec 2011, 19:20

Re: ContentPresenter DataContext bug

20 Dec 2024, 17:10

The ContentPresenters inside the UniformGrid are automatically created by the ItemsControl (the one you marked as "One page (collection of buttons)") and will have its Content (DataContext) set to each of the items of the ItemsSource collection assigned to that ItemsControl.

So you should verify if that ItemsControl has the correct collection attached to the ItemsSource property first.

Who is online

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