prism: problems with dynamically adding tabitems

Jan 23, 2013 at 6:34 PM
Edited Jan 23, 2013 at 6:59 PM

i use the following code in a controller class to add tabitems (which will contain view SubsystemDetailsView) dynamically in the main region in response to some event:

privat void ShowSubsystemDetails(PreCommissioning.Model.Subsystem Subsys)

{

IRegion mainRegion = regionManager.Regions[RegionName.MainRegion];

bool alreadyExists = false ;

foreach (var view in mainRegion.Views)

{

if (view is SubsystemDetailsView ) ;

var subsysdetailsViewModel = ((FrameworkElement)view).DataContext as SubsystemDetailsViewModel;

if (subsysdetailsViewModel.Subsystem == Subsys)

{

mainRegion.Activate(view);

alreadyExists =true;

}

}

if (!alreadyExists)

{

object existingView = mainRegion.GetView("SubsystemDetailsView" );

existingView =ServiceLocator.Current.GetInstance<ISubsystemDetailsView >();

subsystemDetailsViewModel.Subsystem = Subsys;

mainRegion.Add(existingView);

mainRegion.Activate(existingView);

}

}

 

the first part of the code loops through open view so if the view already exist it activates it else control go the second part which create new view in a new tabitem.

my problem is: first part doesn't implement in the right way and tabitems are duplicated and also the tab header that read from view doen't show up.

please help me to fix these problems and if there is better approach to do the same thing (through controller) please advice.

thanks in advance

 

 

 

Developer
Jan 23, 2013 at 8:23 PM
Edited Jan 23, 2013 at 8:23 PM

Hi,

I am not aware of the implementations of your views / view models or the requirements of your scenario, but I made some small modifications to the code snippet you posted above. Please let us know if this helps you solve the problem:

private void ShowSubsystemDetails(PreCommissioning.Model.Subsystem Subsys)  
{
    IRegion mainRegion = regionManager.Regions[RegionName.MainRegion];

    bool alreadyExists = false;
    SubsystemDetailsViewModel subsysdetailsViewModel = null;

    foreach (var view in mainRegion.Views)
    {
        if (view is SubsystemDetailsView)
        {
            subsysdetailsViewModel = ((FrameworkElement)view).DataContext as SubsystemDetailsViewModel;

            if(subsysdetailsViewModel.Subsystem != null && subsysdetailsViewModel.Subsystem == Subsys)
            {
                mainRegion.Activate(view);
                alreadyExists = true;
            }
        }

    }

    if (!alreadyExists)
    {
        object newView = ServiceLocator.Current.GetInstance<ISubsystemDetailsView>();

        subsysdetailsViewModel = ((FrameworkElement)newView).DataContext as SubsystemDetailsViewModel;
        subsysdetailsViewModel.Subsystem = Subsys;

        mainRegion.Add(existingView);
        mainRegion.Activate(existingView);
    }
}

Remember that after setting the Subsystem property you need to raise the PropertyChanged event in order for the view to be notified of this change in the view model.

On the other hand, another possible approach to achieve the scenario you are mentioning could be using navigation instead of view injection to add the views in the above method. By default Prism's navigation API provides capabilities to check if a corresponding view already exist in a Region by using parameters in the navigation request. If a matching view is found, that view is activated in the Region; otherwise, a new view is created and added automatically. Once navigated, you can retrieve the active view in the Region and set the corresponding properties in the view model, if needed.

You can find more information about navigation in the following chapter of Prism documentation:

Damian Cherubini
http://blogs.southworks.net/dcherubini

Jan 24, 2013 at 6:34 AM
Edited Jan 24, 2013 at 9:46 AM

Hi DCherubini,

thanks for your help.my VM is imported into View in the code behind like this:

 

[Import

 public ISubsystemDetailsViewModel VM

{

 get { return DataContext as ISubsystemDetailsViewModel ; }

set { DataContext = value ; }

}

 

 so after applying your modification it through null exception (Object reference not set to an instance of an object.) at

 

 

subsysdetailsViewModel = ((

FrameworkElement)newView).DataContext as SubsystemDetailsViewModel;

 in the second if block because no views already exist.

 **How to replace view injection in my approach by view discovery (using MEF) and Regions but from controller class.

Developer
Jan 24, 2013 at 5:17 PM

Hi,

Based on the code snippets mentioned above, I believe that the exception you mentioned could be caused if the newView instance is not being correctly resolved from the container by the ServiceLocator in the above line:

object newView = ServiceLocator.Current.GetInstance<ISubsystemDetailsView>();

For example you could check if you are correctly exporting the SubsystemDetailsView class to the container and mapping it to the corresponding interface for example using the following attributes:

[Export(typeof(ISubsystemDetailsView))]
[PartCreationPolicy(CreationPolicy.NonShared)]

Also, the view model should be exported similarly if it will  be imported using the ISubsystemDetailsViewModel VM property like you mentioned.

Additionally, I found that in this second block the view added and activated should be the newView instance and not the existingView. Also, if your view model is being imported to the view using the ISubsystemDetailsViewModel, then the DataContext should be cast to this interface and not the specific implementation, for example this could look like this:

if (!alreadyExists)
    {
        object newView = ServiceLocator.Current.GetInstance<ISubsystemDetailsView>();

        subsysdetailsViewModel = ((FrameworkElement)newView).DataContext as ISubsystemDetailsViewModel;
        subsysdetailsViewModel.Subsystem = Subsys;

        mainRegion.Add(newView);
        mainRegion.Activate(newView);
    }

On the other hand regarding the UI Composition approach to use, I don't think that the view discovery approach should be suitable in this case, as based on my understanding of your scenario you will need programmatic control over when the view is created and displayed in the region in which case using view injection may be more appropriate. You could find more information on when to use one approach or the other in the following chapter of the documentation:

I hope this helps,

Agustin Adami
http://blogs.southworks.net/aadami

Jan 24, 2013 at 5:50 PM
Edited Jan 24, 2013 at 5:52 PM

Hi Agustin Adami,

i implemented the fixations you mentioned and the code is now working but still problems exist:

1-when i replace SubsystemDetailsViewModel with ISubsystemDetailsViewModel i get this error:

  Error 2 Cannot implicitly convert type 'PreCommissioning.Module.Subsystem.ViewModel.ISubsystemDetailsViewModel' to 'PreCommissioning.Module.Subsystem.ViewModel.SubsystemDetailsViewModel'. An explicit conversion exists (are you missing a cast?) 

2-still duplication happen in views / tabitems: if i tried to duplicate a tab next to itself it is not duplicated, but if different tab in between it will be duplicated.

3-still the tab header that read from SubsystemDetailsViewModel  not showing, i use following code in xaml in the tab region for

 the ShellWindow (where the view will be displayed)

 <TabControl  prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainRegion}"
                       
                        Cursor="Hand"
                        Grid.Row="0"
                        Grid.Column="1">
                

                    <TabControl.ItemTemplate>

                    <DataTemplate>
                       
                        <StackPanel  Orientation="Horizontal">
                            
                            <TextBlock VerticalAlignment="Center"
                                               Margin="3"
                                               Text="{Binding Path=HeaderInfo}"
                                               />
                           
                            <!--
                            <ContentPresenter
                                Content="{Binding Path=HeaderInfo}"
                                VerticalAlignment="Center" />
                             -->                                                      
                           
                            <Button
                                    Command="{Binding Path=CloseCommand}"
                                    Content="X"
                                    DockPanel.Dock="Right"
                                    Padding="0"
                                    FontSize="8"
                                    Focusable="False"
                                    FontWeight="Bold"
                                    Margin="3, 0, 0, 0"/>
                          
                           
                        </StackPanel>
                       
                    </DataTemplate>
                       
                </TabControl.ItemTemplate>
               
            </TabControl>

please advice.



Developer
Jan 24, 2013 at 8:27 PM

Hi,

It would be helpful if you could provide us with a repro-sample application portraying the behavior you are mentioning so that it could be easier for us to analyze your scenario an help you find the cause behind this error.

Thanks,

Damian Cherubini
http://blogs.southworks.net/dcherubini

Jan 24, 2013 at 8:50 PM
Edited Jan 24, 2013 at 8:52 PM

Hi,

it is LOB application using Sql Server Database through Repository and Service layer.

the scenario is: i have a control panel reside in Navigation Region of ShellWindow. it containes list of systems when i select one EA publish the event for which controller in another module subscribe and trigger the method that contains the above code to display the details of the selected system in the main region.the VM is imported to the view in the code behind and i added above the xaml code for the tabcontrol that represent main region in which code will be displayed.

I hope this clarification could help. 

 

Developer
Jan 24, 2013 at 10:20 PM

Hi,

As a starting point, regarding the problem to show the corresponding Tab Header, I believe you could check the bindings used inside the DataTemplate, like the TextBlock that defines the header name, take into account that you can't directly bind view models properties from inside a DataTemplate like shown above.

For example I believe you could try the approach mentioned in this blog post , in other words the binding in your TextBlock could result in something like this if you want to bind the HeaderInfo property exposed by the view model:

<TextBlock VerticalAlignment="Center" Margin="3" Text="{Binding Path=DataContext.DataContext.HeaderInfo, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabItem}}}"/>

Also, a similar binding should be applied to the button command.

As an alternative approach you could also use Style setters to define the TabControl.ItemContainerStyle, as an example of this approach I believe you could check the ImageInTabHeaderSample.zip sample proposed in this Work Item.

Regarding the duplicated views so far, I couldn't find the cause of this behavior based on the information you provided.

Regards,

Agustin Adami
http://blogs.southworks.net/aadami

Jan 25, 2013 at 5:39 AM
Edited Jan 25, 2013 at 12:18 PM

Hi,

1-Thanks Adami it is another problem solved, the problem of showing HeaderInfo in the Tab Header.

2-For the tabs duplication problem:

 i found when inserting Breakpoint @ open { of the following block

 if (subsysdetailsViewModel.Subsystem != null && subsysdetailsViewModel.Subsystem == Subsys)
                    {
                        mainRegion.Activate(view);
                        alreadyExists = true;
                    }

i found that the breakpoint is hit only in one situation when i try to open same tabitem following to itself but if there is different tab in between the brakpoint is not hit meaning that condition "subsysdetailsViewModel.Subsystem == Subsys" compare current tab with the previous one only not with all open tabs.

**May be the View (SubsystemDetailsView) and the view model (SubsystemDetailsViewModel) should not be constructed

   by the container??

3-If there is diffrent apprach like using  UriQuery class or so, may it will be simpler or better

thanks for your always useful help.

 

Developer
Jan 25, 2013 at 3:20 PM
Edited Jan 25, 2013 at 3:21 PM

Hi,

After analyzing this in further detail, I believe that the reason of the duplicated views behavior is that subsysdetailsViewModel.Subsystem == Subsys condition will be true only if you pass the same instance of the Subsystem class and it will not consider if they inner values match. And as each view model may have its own instance of this class with its corresponding values. Then I believe you should consider filtering this, by using for example and Id property in this class. For example like this:

if (subsysdetailsViewModel.Subsystem != null && subsysdetailsViewModel.Subsystem.ID == Subsys.ID)
{
... }

On the hand, I believe that an alternative to this approach which uses a URI to identify the view to be displayed, could be by benefiting of the Prism's navigation API as Damian Cherubini mentioned above. This approach will let you pass a parameter during the navigation request which could be used to check if the existing views in the region will be able to handle the the navigation request or not (by using the IsNavigationTarget method in the INavigationAware interface which can be implemented in your views / view models), this way for example you could pass an ID as the parameter, which could identify your specific Subsystem to check if the view already exist, or if not you could use the OnNavigatedTo method defined in this same interface to initialize the newly displayed view based on the parameter passed.

For more detailed information on how to implement this particular approach you could check the View-Based Navigation sections of the documentation.

I hope you find this handy,

Agustin Adami
http://blogs.southworks.net/aadami

Jan 25, 2013 at 3:59 PM

Hi,

1-first i want to thank Agustin Adami & Damian Cherubini for their advice and persistance about solving the problems i'm facing.

2-I modified the condition to "

subsysdetailsViewModel.Subsystem.SubsystemNo == Subsys.SubsystemNo"

where SubsystemNo is like id but duplication still exist.i think no problem with the condition with its previous state because

as i mentioned above it working in one case when i try to duplicate tabitem next to itself, duplication eleminated,

but if different tab in between duplication take place meaning that that condition compare the new tab under creation

with the previous one only not with all open tabs maybe there is a problem with looping through all open tabs.may be becuase

MEF create the objects where it should be created through view outside mef ...

Developer
Jan 25, 2013 at 5:37 PM

Hi,

I created a small spike application based in the approach mentioned above. Perhaps, you could find it useful to compare with your own implementation and check the differences with it.

You can find it in my Skydrive account  under the name: TabControlDynamicTabItems.

Regards,

Agustin Adami
http://blogs.southworks.net/aadami

Jan 25, 2013 at 6:01 PM

Hi,

thanks for your effort and concern i will compare carefully your spike with my code to find out where the problem is

then i will give you the feedback.

Jan 25, 2013 at 8:26 PM
Edited Jan 25, 2013 at 9:09 PM

Hi,

OOOOK, i found the bug, and i'm sorry for wasting your time and my time

the error is

[Export(typeof(ISubsystemDetailsViewModel))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class SubsystemDetailsViewModel:INotifyPropertyChanged, ISubsystemDetailsViewModel

{...............

that what caused duplication of tabitems.

thank you Adami also thank you DCherubini. now i can sleep .........for today [:)]