View Issue Details

IDProjectCategoryView StatusLast Update
0001639NoesisGUIC++ SDKpublic2020-03-20 16:38
ReporterstevehAssigned Tosfernandez 
PrioritynormalSeverityblockReproducibilityalways
Status resolvedResolutionfixed 
Product Version2.2.6 
Target Version3.0.0Fixed in Version3.0.0 
Summary0001639: Assert when connecting element to a different view
DescriptionHi guys, I'm trying to update to 2.2.6 but some of our code is asserting. Bear with me, this will take a bit of explaining...

I've created a new control derived from ContentControl called "ExplicitlyControlledElement". The purpose of this control is to allocate a new view and move the children of it's element out of the main view and into a newly allocated view which can be controlled by a different controller. I did it this way to simplify screens which may be controlled by multiple people. This means that we can do something like this in the main XAML:

<usercontrol>
    <local:UniquelyControlledElement>
        <stackpanel KeyboardNavigation.DirectionalNaviation="Contained">
            <button Content="A" />
            <button Content="B" />
        </stackpanel>
    </local:UniquelyControlledElement>
</usercontrol>

At runtime, in the OnInitialised delegate of the UniquelyControlledElement, we allocate a new view and move the content of the children into the root of the new view. Something like this:

I've attached our actual function to this bug report called "UniquelyControlledElement_CreateView.cpp". Hopefully you should get the general idea of what I'm trying to do. Note: There is a also an additional control called a "DummyPositionalObject". This gets inserted into the hierarchy so if we animate the position of the UniquelyControlledElement in the main view we move the view as well, so the UI artist can still move the position of these views in animations.

This all used to work fine in 2.2.1. However, I've just updated to 2.2.6 and I'm getting an assert when this gets called here:

void Visual::OnConnectToView(IView* view)
{
    // A visual can only be attached to one view
    NS_ASSERT(view != 0);
    NS_ASSERT(mView == 0); // <-- Here
    NS_ASSERT(!mIndex);

    mView = static_cast<View*>(view);

    if (CheckVisualFlag(VisualFlags_NeedsUpdate))
    {
        AddInvalidatedVisual();
    }

    SetVisualFlag(VisualFlags_ParentChanged | VisualFlags_CreateRenderNode);
}

So this looks like something has already moved the view across. I've added a bunch of debugging and I think I've tracked it down, so here is what I believe is happening:

The element at fault is a control with a dynamic template applied, specifically it looks like this (with some slight name adjustments):

<Control x:Name="Icon_Locked" HorizontalAlignment="Center" Height="67.333" VerticalAlignment="Center" Width="67.333" Template="{DynamicResource Icons_PopUp_Locked}" Foreground="#FF7C7C7C" Margin="47,14,10,40" Focusable="False"/>

The Icons_PopUp_Locked is in a different database and I've attached a copy of what this looks like (with the path data removed to simplify the problem). The issue comes from the DynamicResource...

1. The Icon_Locked control's Visual::ConnectToView is called from the parent grid
2. FrameworkElement::OnConnectToView is called from Visual::ConnectToView
3. It calls the OnAncestorChanged delegate
4. It calls into DynamicResourceExpression::OnAncestorChanged which has been bound because the template is set up to be a dynamic resource.
5. DynamicResourceExpresion::OnAncestorChanged calls InvalidateProperty with the "Template" property
+ NsSymbolToString((*((Noesis::DependencyProperty*)mTargetProperty)).mName.mIndex) 0x0000000052ba0fa0 "Template" const char *
6. DependencyObject::InternalInvalidateProperty is called with the template dependency property passed in. It makes it way to the bottom of the function where this code is executed:

        Expression* expression = DynamicCast<Expression*>(pv.boxed.GetPtr());

        if (expression)
        {
            // Connect new target
            Ptr<Expression> cloned = IsInitialized() ? expression->Reapply(this, dp) :
                Ptr<Expression>(expression);

            InternalSetExpression(dp, cloned.GetPtr(), pv.providerPriority);
        }

7. DependencyObject::InternalSetExpression calls "dp->SetValueObject(this, value, priority, newExpression, metadata);"
8. This ultimately ends up triggering a PropertyChangedNotify call with the TemplateProperty
9. The Control::OnPropertyChanged gets executed and the args.prop matches the TemplateProperty
10. This calls FrameworkElement::ApplyFrameworkTemplate.
11. This calls SetSingleVisualChild on the new root
12. This ultimate connects the child to the view of the parent, which has already been moved across to the new root in bullet point 2. (See first_callstack.txt attachment)
13. We finish this and go all the way back out the call stack to Visual::ConnectToView
14. Visual::ConnectToView calls Visual::OnConnectToViewChildren
15. The Control now iterates across all children and tries to connect it to the new view.
16. We assert on the first child because the ApplyFrameworkTemplate already parented the node, so we shouldn't connect the child to the view, it's already been done. (see second_callstack.txt attachment).

*phew* we made it :)

Now.. I'm not sure on the best fix here. Perhaps it could be fixed by the same way you prevent infinite loops in the measure by adding a visual flag like "InReparenting", and before you do any logic in Visual::ConnectToView you call something like CheckVisualFlag(!InReparenting), e.g. maybe something like this:

void Visual::ConnectToView(IView* view)
{
    NS_ASSERT(view != 0);
    if (!CheckVisualFlag(VisualFlags_InReparenting))
    {
        SetVisualFlag(VisualFlags_InReparenting);
        OnConnectToView(view);
        OnConnectToViewChildren();
        ClearVisualFlag(VisualFlags_InReparenting)
    }
}


But I'm not sure if this'll bust anything during the framework application as I'm not sure what side effect will be caused by not calling OnConnectToView when applying the template mid reparenting.

This issue is preventing me from integrating 2.2.6 so I'd like to get a fix for this pretty shortly if at all possible, so any suggestions would be great.

Cheers,

-Steven
Steps To Reproduce* see description *
TagsNo tags attached.
PlatformAny

Activities

steveh

steveh

2020-03-14 18:18

reporter  

ResourceDatabase.xaml (723 bytes)
UniquelyControlledElement_CreateUniqueView.cpp (2,106 bytes)
second_callstack.txt (833 bytes)
This is the second callstack of where the same child element tries to reparent the view. However we assert because this was already set in the first callstack.

>	Game_PcDx11_x64_Debug.exe!Noesis::Visual::OnConnectToView(Noesis::IView * view) Line 660	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::UIElement::OnConnectToView(Noesis::IView * view) Line 2309	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::FrameworkElement::OnConnectToView(Noesis::IView * view) Line 1955	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::Visual::ConnectToView(Noesis::IView * view) Line 191	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::Visual::OnConnectToViewChildren() Line 718	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::FrameworkElement::OnConnectToViewChildren() Line 1993	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::Visual::ConnectToView(Noesis::IView * view) Line 192	C++
second_callstack.txt (833 bytes)
first_callstack..txt (3,879 bytes)
This is the first callstack of where the child elements have the root set to the parent's new root (the new view). 

 	Game_PcDx11_x64_Debug.exe!Noesis::Visual::OnConnectToView(Noesis::IView * view) Line 653	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::UIElement::OnConnectToView(Noesis::IView * view) Line 2309	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::FrameworkElement::OnConnectToView(Noesis::IView * view) Line 1955	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::Visual::ConnectToView(Noesis::IView * view) Line 191	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::Visual::AddVisualChild(Noesis::Visual * child) Line 280	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::FrameworkElement::SetSingleVisualChild(Noesis::Visual * child) Line 1441	C++
	Game_PcDx11_x64_Debug.exe!Noesis::FrameworkElement::ApplyFrameworkTemplate(Noesis::FrameworkTemplate * frameworkTemplate) Line 727	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::Control::OnPropertyChanged(const Noesis::DependencyPropertyChangedEventArgs & args) Line 452	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::DependencyObject::NotifyPropertyChanged(const Noesis::DependencyProperty * dp, Noesis::StoredValue * storedValue, const void * oldValue, const void * newValue, bool valueChanged, bool isBaseComponent, const Noesis::PropertyMetadata * metadata) Line 1188	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::DependencyObject::InternalSetValue(const Noesis::DependencyProperty * dp, void * oldValue, const void * newValue, void * coercedValue, unsigned char priority, Noesis::Expression * newExpression, const Noesis::PropertyMetadata * metadata, Noesis::Value::Destination destination, bool isBaseComponent) Line 830	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::ValueStorageManagerImpl<Noesis::Ptr<Noesis::BaseComponent> >::SetValue(Noesis::DependencyObject * dob, const Noesis::DependencyProperty * dp, Noesis::BaseComponent * value, unsigned char priority, Noesis::Expression * expression, const Noesis::PropertyMetadata * metadata, Noesis::Value::Destination destination) Line 234	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::ValueStorageManager::SetValueObject(Noesis::DependencyObject * dob, const Noesis::DependencyProperty * dp, Noesis::BaseComponent * value, unsigned char priority, Noesis::Expression * expression, const Noesis::PropertyMetadata * metadata, Noesis::Value::Destination destination) Line 40	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::DependencyProperty::SetValueObject(Noesis::DependencyObject * obj, Noesis::BaseComponent * value, unsigned char priority, Noesis::Expression * expression, const Noesis::PropertyMetadata * metadata, Noesis::Value::Destination destination) Line 210	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::DependencyObject::InternalSetExpression(const Noesis::DependencyProperty * dp, Noesis::Expression * newExpression, unsigned char priority) Line 632	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::DependencyObject::InternalInvalidateProperty(const Noesis::DependencyProperty * dp, unsigned char priority) Line 1046	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::DependencyObject::InvalidateProperty(const Noesis::DependencyProperty * dp, unsigned char priority) Line 236	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::DynamicResourceExpression::OnAncestorChanged(Noesis::FrameworkElement * __formal) Line 228	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::Delegate<void __cdecl(Noesis::FrameworkElement *)>::MemberFuncStub<Noesis::DynamicResourceExpression,void (__cdecl Noesis::DynamicResourceExpression::*)(Noesis::FrameworkElement *)>::Invoke(Noesis::FrameworkElement * <args_0>) Line 465	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::Delegate<void __cdecl(Noesis::FrameworkElement *)>::operator()(Noesis::FrameworkElement * <args_0>) Line 155	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::FrameworkElement::OnConnectToView(Noesis::IView * view) Line 1966	C++
 	Game_PcDx11_x64_Debug.exe!Noesis::Visual::ConnectToView(Noesis::IView * view) Line 191	C++
first_callstack..txt (3,879 bytes)
steveh

steveh

2020-03-14 18:34

reporter   ~0006147

I realised I was being silly in my suggestion, I don't need to CheckFlag on the current visual, I need to check the parent's flag. After realising this, I changed the function to look like this (it's not nice, don't judge :) )

void Visual::ConnectToView(IView* view)
{
    NS_ASSERT(view != 0);
    if (nullptr == GetVisualParent() || !GetVisualParent()->CheckVisualFlag(VisualFlags_InReparenting)) // #SPH_TO_REMOVE
    { // #SPH_TO_REMOVE
        SetVisualFlag(VisualFlags_InReparenting); // #SPH_TO_REMOVE
        OnConnectToView(view);
        ClearVisualFlag(VisualFlags_InReparenting); // #SPH_TO_REMOVE
        OnConnectToViewChildren();
    } // #SPH_TO_REMOVE
}

The lines with #SPH_TO_REMOVE were the ones I added. This did actually work for me, and I've managed to load my XAML elements and everything still seems to work upon initial inspection. I'm working remotely right now so I can't actually check to see if the second controller can control the other view, but the primary user can still control the first view. I can't see any major issues so far, but I've not actually ran through the entire game yet. I'll give it more testing. I'm sure you guys will know if this is a silly thing to do or not though.

Cheers,

-Steven
sfernandez

sfernandez

2020-03-17 12:38

manager   ~0006150

Hi Steve, we are looking into this as it seems something we accidentally introduced in 2.2.6 and is still occurring in 3.0 version.
We'll let you know as soon as we have the proper solution.

Thank you so much for the detailed report, it will for sure help a lot.
sfernandez

sfernandez

2020-03-18 19:30

manager   ~0006151

Hi Steve,

I fixed this in branch 2.2 revision 8936, could you please give it a try and confirm?
steveh

steveh

2020-03-18 21:57

reporter   ~0006158

Hi there, yeah, that's seemed to have done the trick. I've reverted my hack and replaced it with the submitted changelist and it seems to work great. Cheers!

Issue History

Date Modified Username Field Change
2020-03-14 18:18 steveh New Issue
2020-03-14 18:18 steveh File Added: ResourceDatabase.xaml
2020-03-14 18:18 steveh File Added: UniquelyControlledElement_CreateUniqueView.cpp
2020-03-14 18:18 steveh File Added: second_callstack.txt
2020-03-14 18:18 steveh File Added: first_callstack..txt
2020-03-14 18:34 steveh Note Added: 0006147
2020-03-17 12:35 sfernandez Assigned To => sfernandez
2020-03-17 12:35 sfernandez Status new => assigned
2020-03-17 12:35 sfernandez Target Version => 3.0.0
2020-03-17 12:35 sfernandez Description Updated View Revisions
2020-03-17 12:38 sfernandez Status assigned => feedback
2020-03-17 12:38 sfernandez Note Added: 0006150
2020-03-17 12:38 sfernandez Status feedback => assigned
2020-03-18 19:30 sfernandez Status assigned => feedback
2020-03-18 19:30 sfernandez Note Added: 0006151
2020-03-18 21:57 steveh Note Added: 0006158
2020-03-18 21:57 steveh Status feedback => assigned
2020-03-20 16:38 sfernandez Status assigned => resolved
2020-03-20 16:38 sfernandez Resolution open => fixed
2020-03-20 16:38 sfernandez Fixed in Version => 3.0.0