SL4 Navigation Pages + MEF

Dec 14, 2009 at 12:21 AM

I made a simple SL4 (VS2010) app using the Silverlight Business Application template. I have added a menu item and have the link loading an additional page. I am using MEF to insert  a Silverlight user control into the page. The control is added properly the first time I access the page, but when I navigate away and then back again, I get the "Element is already a child of another element" error in the TargetFrame.items.add(widget.value) line. I understand why this is happening, but I haven't been able to figure out how to resolve the problem. Can anyone help? The key part of the code is:
PartInitializer.SatisfyImports(this);
foreach (var widget in Widgets)
TargetFrame.Items.Add(widget.Value);  //using lazy loading
Thanks.

Dec 14, 2009 at 4:00 PM

HI

Most likely this error is because the widget is already the child of another parent element. This could happen if the widget is a window.

Glenn

Dec 14, 2009 at 5:54 PM
Edited Dec 15, 2009 at 1:58 PM

Hi back,

Your PDC demo got me excited.

I am attempting to use MEF instead of adding a custom user control to the page in the normal way with a reference to the control in the page's xaml. Using MEF would be much better to use for large scale applications with its ability to be "discovered" and then automatically used, updated as needed, and dynamically loaded (even dragged and dropped) with less effort than re-deploying a new custom user control in the usual way. But I would like to also take advantage of Silverlight Navigation and the benefits of that also.

Let me be more specific with a simple example:

I create a new project in VS2010 (BusinessApplication1) using the Business Application Template.

I add a Silverlight User Control (SilverlightControl1) to the Controls folder and add the Export attribute 

Export(typeof(UserControl))]

I add the reference using System.ComponentModel.Composition;

For simplicity, I add a label to the control that displays "My Control". That is all for the control.

I add to About.xaml the following within the StackPanel (ContentStackPanel)

<Border><ItemsControl x:Name="Target" Height="Auto"></ItemsControl></Border>

I add the reference to About.xaml.cs:

using System.ComponentModel.Composition;

I add after this.Title = ApplicationStrings.AboutPageTitle;:

PartInitializer.SatisfyImports(this);

foreach (var widget in Widgets)

Target.Items.Add(widget.Value); 

I add above /// <summary> /// Executes when the user navigates to this page.:

ImportMany()]

public Lazy<UserControl>[] Widgets { get; set; }

Then I build and run the app. I click About and my control shows properly. I click Home and the home page shows. I click About again and I get the "Element is already a child..." error.

In summary, I'd like to know how to use MEF to add Silverlight User Controls to a navigation page without getting this error. The widget is remaining as a child to the page and there must be a way to release the reference and renew a new one when I return to the page, or some other approach.

Mar 4, 2010 at 7:27 PM

Having same issue, any solution found?

Mar 4, 2010 at 7:32 PM

@mewald You need to clear the Target items collection before you do your foreach, otherwise you are attempting to add the same widget to the target twice which will fail.

Glenn

Mar 4, 2010 at 7:50 PM

I'm clearing out my content control but still having same issue.  When i first navigate to page its fine, if I leave and then come back is when I get the error.

[ImportMany(AllowRecomposition=true)]

     public Lazy<ISearchGrid,ISearchGridMetaData>[] SearchGrids { get; set; }

<!--EndFragment-->

 

public void OnImportsSatisfied()

       {

           _currentSearchGrid = null;

           hdrGrid.Content = null;

 

           foreach (var grid in SearchGrids)

           {

 

               if (App.Current.CurrentApplication.APPLICATION_NAME.Equals(grid.Metadata.App))

               {

 

                   _currentSearchGrid = grid.Value;

                   

                   hdrGrid.Content = CreateOverlayLayer(GridClick, _currentSearchGrid as FrameworkElement);

                                 }

           }

 

       }

<!--EndFragment-->
Mar 5, 2010 at 11:54 AM

Any ideas out there?  This seems like a bug. 

Mar 5, 2010 at 1:47 PM

I took the hello mef sample and modified to demonstrate the problem.  Sample can we found here

http://cid-85042150b606fcfd.skydrive.live.com/browse.aspx/Samples

 

Developer
Mar 5, 2010 at 10:05 PM

FYI: My comments are from reading the code in home.xaml.cs from your skydrive not from the fragment you posted above.

[ImportMany(AllowRecomposition = true)]
public Lazy<UserControl, IWidgetMetadata>[] Widgets { get; set; }

public void OnImportsSatisfied()
{
    TopWidgets.Items.Clear();
    BottomWidgets.Items.Clear();

    foreach (var widget in Widgets)
    {
        if (widget.Metadata.Location == WidgetLocation.Top)
            TopWidgets.Items.Add(widget.Value);
        else if (widget.Metadata.Location == WidgetLocation.Bottom)
            BottomWidgets.Items.Add(widget.Value);
    }
}
The problem is that a UserControl can only be parented (*Widgets.Items.Add causes it to be parented) once in its lifetime and on each recomposition you are adding the same object instances that previously existed to the collection so they are being reparented which is what causes the error.
There are a couple possible solutions:
1) Instead of clearing on the Items collection on recomposition simply check to see if it already exists in the collection and if so then don't add it again (however keep in mind that removals can happen with recomposition as well so you might need to somehow track the widgets you need to remove).
2) If you have no state stored in your widgets (which may be a bad assumption to make) you could have MEF essentially recreate all the widgets each time by specifying RequiredCreationPolicy=CreationPolicy.NonShared on your ImportMany attribute.
My suggestion would be to go with option #1 and just always check the collection and only add it if it hasn't already been added. 

Hope that helps,
Wes
Mar 5, 2010 at 10:21 PM

The bug in this case must be in my code as this was copied from my sample. It's interesting that I currently don't have this issue and the app does recompose when you download new XAPs, which adds new elements to the UI. In that case clear appers to work fine. Are you using SL4 or SL3 bits?

Thanks

Glenn

Mar 6, 2010 at 4:00 AM

I would think that calling Clear on the items should removed it from being parented?

Mar 6, 2010 at 4:05 AM
Edited Mar 6, 2010 at 4:07 AM

Glenn, i'm using vs2008 and sl3.  If you just run the app you will see that recompositon happens when you first load and widgets get added to the collection.  If you navigate to the about page and then return to the home page, recomposition happens again but the widget is still parented and can't be added to the collection even though clear was called and you get the error.

Mar 7, 2010 at 4:06 PM
Edited Mar 7, 2010 at 4:09 PM

First off, I am delighted that this issue is being discussed further. I thought that I must have been the only one experiencing this problem and that was why I got no response. I had all but given up on using navigation with MEF. Glenn, I tried clearing the target items collection long ago, but it still produces the same error.  The problem appears to be with the ItemsControl and not the items. The ItemsControl is somehow being persisted (or cached) between navigation page changes. I have been assuming that it had to do with Navigation, so I stopped using it. I have run my example in both VS2008 with SL3 and with VS2010 with SL4 with the same results. Could this be somehow due to Navigation? If I don't use Navigation and just compose new pages, MEF works fine, but for my applications it would be better to have a bunch of navigation pages that get populated using MEF.

Mar 7, 2010 at 6:50 PM

The following code is a work-around to the problem.   Since the page is being cached it is an instance holding onto a reference of the widget (thus the error).   The key then is to clear out the two list "on the cached instance" not the current one.   To minimize code I piggy back on IDisposable (not fully implementing the disposable pattern - just to get the point across).

http://cid-85042150b606fcfd.skydrive.live.com/browse.aspx/Samples 

   18 public partial class Home : Page, IPartImportsSatisfiedNotification, IDisposable

   19 {

   20     [ImportMany(AllowRecomposition = true)]

   21     public Lazy<UserControl, IWidgetMetadata>[] Widgets { get; set; }

   22 

   23     public static IDictionary<Enum, Page> widgetList =

   24         new Dictionary<Enum, Page>();

   25 

   26     public Home()

   27     {

   28         InitializeComponent();

   29         CompositionInitializer.SatisfyImports(this);

   30     }

   31 

   32     public void OnImportsSatisfied()

   33     {

   34         foreach (var widget in Widgets)

   35         {

   36             //::::::::::::::[ Track widgets ]:::::::::::::::

   37             WidgetLocation location = widget.Metadata.Location;

   38             var item = widgetList.FirstOrDefault<KeyValuePair<Enum, Page>>

   39                 (p => p.Key.Equals(location));

   40             if (item.Key != null)

   41             {

   42                 ((IDisposable)item.Value).Dispose();

   43                 widgetList.Remove(item);

   44             }

   45             widgetList.Add(location, this);

   46             //::::::::::::::[ end tracking ]::::::::::::::::

   47 

   48 

   49             if (widget.Metadata.Location == WidgetLocation.Top)

   50                 TopWidgets.Items.Add(widget.Value);

   51 

   52             else if (widget.Metadata.Location == WidgetLocation.Bottom)

   53                 BottomWidgets.Items.Add(widget.Value);

   54         }

   55     }

   56 

   57     /// <summary>

   58     /// Clear out the widgets

   59     /// </summary>

   60     public void Dispose()

   61     {

   62         TopWidgets.Items.Clear();

   63         BottomWidgets.Items.Clear();

   64     }

   65 }

<!--EndFragment-->
Mar 7, 2010 at 7:54 PM
Edited Mar 7, 2010 at 8:03 PM
Cool Bill.

The one downside of that approach is it forces instances to get created. I originally wanted everything lazy such that i can show if there is a widget that does not have the values I want, I don't create it. Now one way to deal with that is to attach a WidgetID piece of metadata. Then keep track of which widgets were added by keeping a collection of WidgetIDs which you can match againt. This way you can still filter.

This gets into a whole other realm which relates to part identity in open systems, but I won't go there.

Glenn

More like the following?   (just to provide a complete example for the community)

http://www.global-webnet.net/Samples/HelleMefNavR1.zip  

   18 public partial class Home : Page, IPartImportsSatisfiedNotification, IDisposable

   19 {

   20     [ImportMany(AllowRecomposition = true)]

   21     public Lazy<UserControl, IWidgetMetadata>[] Widgets { get; set; }

   22 

   23     public static IDictionary<string, Page> widgetList =

   24         new Dictionary<string, Page>();

   25 

   26     public Home()

   27     {

   28         InitializeComponent();

   29         CompositionInitializer.SatisfyImports(this);

   30     }

   31 

   32     public void OnImportsSatisfied()

   33     {

   34         foreach (var widget in Widgets)

   35         {

   36             if (widget.Metadata.Location == WidgetLocation.Top)

   37                 TopWidgets.Items.Add(TrackWidget(widget));

   38 

   39             else if (widget.Metadata.Location == WidgetLocation.Bottom)

   40                 BottomWidgets.Items.Add(TrackWidget(widget));

   41         }

   42     }

   43 

   44     private UserControl TrackWidget(Lazy<UserControl,IWidgetMetadata> widget)

   45     {

   46         // Check our static tracking list for our WidgetId

   47         var item = widgetList.FirstOrDefault<KeyValuePair<string, Page>>

   48             (p => p.Key.Equals(widget.Metadata.WidgetId));

   49 

   50         // If found then Dispose it's contents and remove it from

   51         // our list (we'll add it with our new instance)

   52         if (item.Key != null)

   53         {

   54             ((IDisposable)item.Value).Dispose();

   55             widgetList.Remove(item);

   56         }

   57         // Add new instance

   58         widgetList.Add(widget.Metadata.WidgetId, this);

   59         return widget.Value;

   60     }

   61 

   62     /// <summary>

   63     /// Clear out the widgets

   64     /// </summary>

   65     public void Dispose()

   66     {

   67         TopWidgets.Items.Clear();

   68         BottomWidgets.Items.Clear();

   69     }

   70 }

Mar 7, 2010 at 8:56 PM

After a bit of playing, I found something else that appears to work. Clear the collection before navigating off the page by adding the following to the Home codebehind. This removes the need for needing the metadata I was talking about.

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
      TopWidgets.Items.Clear();
      BottomWidgets.Items.Clear();
}

While I was looking into this, I decided to do a refactored version that uses a ViewModel and binding rather than imperatively populating the UI. This is a much cleaner way to accomplish the same thing. I still needed to use the above code.

[Export]
public class HomeViewModel : IPartImportsSatisfiedNotification, INotifyPropertyChanged
{
    [ImportMany(AllowRecomposition = true)]
    public Lazy<UserControl, IWidgetMetadata>[] Widgets { get; set; }

    public IEnumerable<UserControl> TopWidgets { get; private set; }

    public IEnumerable<UserControl> BottomWidgets { get; private set; }

    public void OnImportsSatisfied()
    {
        TopWidgets = Widgets.Where(w => w.Metadata.Location == WidgetLocation.Top).Select(i=>i.Value);
        BottomWidgets = Widgets.Where(w => w.Metadata.Location == WidgetLocation.Bottom).Select(i=>i.Value);
        OnPropertyChanged("TopWidgets");
        OnPropertyChanged("BottomWidgets");
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string property)
    {
        var handler = PropertyChanged;
        if (handler != null)
            PropertyChanged(this, new PropertyChangedEventArgs(property));
            
    }
}

I placed an updated version using 2010 (but still SL3) on my skydrive here:

 


 

Mar 8, 2010 at 11:27 AM

Very nice Glenn! 

Mar 8, 2010 at 2:06 PM

Thanks, Glenn. Putting the Clear operation in OnNavigatedFrom solved my problem. This is a big relief for me and I really appreciate it.

Mar 8, 2010 at 2:45 PM

Great work around, Thanks!!

Is this something that is always going to have to be done going forward when using MEF with SL navigation?

Mar 8, 2010 at 11:05 PM
Edited Mar 8, 2010 at 11:09 PM

Glenn mentioned this thread to me, so I figured I'd chime in :)

The correct solution here is as Glenn suggested:

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
      TopWidgets.Items.Clear();
      BottomWidgets.Items.Clear();
}

The reason this is necessary is that SL Navigation creates a new instance of the page every time you navigate to it (by default).  As a result, at no point were the imports cleared from TopWidgets and BottomWidgets in the original container.  The next time you navigated to the URI, the same imports were being placed in new TopWidgets and BottomWidgets instances (created when the new instance of the page was created).  This resulted in the same UI elements being given multiple parents, which is an error.

If you want SL Navigation not to create new instances of the page with every navigation, use NavigationCacheMode on the page itself:

<navigation:Page x:Class="HelleMefNav.Home" NavigationCacheMode="Required"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" ... />

This will cause the Navigation framework to re-use the page whenever the same URI is navigated to, and will make your original logic (clearing TopWidgets and BottomWidgets when the imports are satisfied) sound as long as HelloMefNav.Home is the only page that uses those imports.

A word of warning, however: if NavigationCacheMode is Required, the page instance will survive as long as the frame that cached it does.  This is often the lifetime of your application.

Consider using OnNavigatedFrom (or else don't try to share UI elements across pages :)) -- it's intended as a place where you can perform cleanup when your page is going away.  If you'd like to optimize around this, you can set NavigationCacheMode to "Enabled".  This will cause the Frame control to cache up to Frame.CacheSize pages (and remove items from the cache based upon a least-recently-used strategy as you navigate to other pages).

 

I hope this helps clarify what's going on!  Feel free to let me know if you have other questions about Navigation in SL!

-David Poll

Mar 9, 2010 at 2:55 AM

One of the odd things that I observed in the navigation app mentioned in this thread was that CompositionInitializer.SatisfyImports() holds the references to the parts being initialized for the lifetime of the application in a list. This list is in ComposablePartExportProvider and is called _parts (private variable). The list grows as we navigate to different pages or even the same page. I created a sample app with just Home and About pages and noticed that navigating back and forth between the two pages adds the Home and About instances to the _parts list. I ended up having 10 instances of Home and 10 instances of About just by navigating between Home And About 10 times. I am relatively new to MEF and was wondering if I am doing something wrong. If not is there an alternative to CompositionInitializer.SatisfyImports() that does not keep these instances in memory.

Mar 9, 2010 at 4:43 AM

CompositionInitializer does hold references as you mentioned. If you navigate back and forth it will continue to grow the collection. One approach it sounds like is to use NavigationCacheMode as David mentioned. This will prevent the user control from getting created over and over.

Alternatively you have a few options:

1. Expose the container (CompositionContainer) and use it directly. The container has a ComposeParts extension method which accepts a params of ComposablePart instances. From an existing user control you can call AttributedModelServices.CreatePart passing in the instance of the control in order to get a part. You can then pass the part to ComposeParts to have it composed simliar to CompositionInitializer. The difference is you can save the part, and then call RemovePart on the container in the OnNavigatedFrom method to have it removed.

2. Use ExportFactory<T> to manufacture instances where T is the contract that you are creating. You can import an export factory on the application for example ExportFactory<HomeView>. You can then call the Factory to create a new HomeView instance from the container. The context object the factory returns implements IDisposable. Once you call Dispose on it, it will remove the export / part from the container. One challenge of using ExportFactory<T> is that I am not sure that the Navigation apis allow you to override creation of the Navigation view. In that case to use ExportFactory you'll want to have an inner view within HomeView that HomeView creates using the factory and releases. I'll talk with David about this to see if there's any options such as using the OnNavigatedTo method.

Glenn

 

Mar 9, 2010 at 4:44 AM

Thanks for weighing in David!

Mar 9, 2010 at 6:12 AM

Thanks for the quick reply Glen. I really like the first approach of exposing the container. As you mentioned I have the option of calling the RemovePart method in the OnNavigateFrom method. I was wondering if it is safe to ComposeParts so that the imports are filled in and then immediately call RemovePart method. The reason I ask this is because we are using SatisfyImports method of the CompositionInitializer in places other than navigation where I dont have something like OnNavigatedFrom. I tried calling RemovePart after ComposeParts and it worked. I guess I might run into problems in Imports that are recomposable. Apart from that I think I should be ok. Let me know your thoughts...

Vaibhav