View Injection inside ItemsControl ItemTemplate

Topics: Prism v2 - Silverlight 3
Dec 16, 2009 at 9:38 PM

This is the first application that myself and a co-worker are developing using Prism and MVVM in Silverlight 3.0. I am working on a shell/framework for the project that will have a list of "plugins" that can be added to a "workspace". Each "plugin" implements IWorkspacePlugin which for now looks like:

public interface IWorkspacePlugin
{
    string PluginName { get; set; }
    object PluginView { get; set; }
    object PluginSettingsView { get; set; }
    DelegateCommand<object> ClosePluginCommand { get; set; }
}

The intent of "PluginView" is to store the plugin's view.

I then have the region in a view where these plugins should be added. The region's view looks like the following (some things omitted for brevity):

<ListBox x:Name="ModuleListBox"
             Grid.Row="1"
             rgn:RegionManager.RegionName="Workspace"
             Background="Yellow"
             ItemsSource="{Binding Plugins}">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
        <ListBox.Template>
            <ControlTemplate>
                <Grid x:Name="ListBoxGrid">
                    <ItemsPresenter></ItemsPresenter>
                </Grid>
            </ControlTemplate>
        </ListBox.Template>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Border BorderBrush="Black"
                        BorderThickness="2"
                        Margin="0"
                        Padding="0">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="20"></RowDefinition>
                            <RowDefinition Height="*"></RowDefinition>
                            <RowDefinition Height="5"></RowDefinition>
                        </Grid.RowDefinitions>
                        <Grid Grid.Row="0">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"></ColumnDefinition>
                                <ColumnDefinition Width=".05*"></ColumnDefinition>
                            </Grid.ColumnDefinitions>
                            <Thumb HorizontalAlignment="Stretch"
                                   Background="Green"
                                   DragDelta="Thumb_DragDelta"></Thumb>
                            <Button Content="X"
                                    HorizontalAlignment="Right"
                                    Grid.Column="1"
                                    cmd:Click.Command="{Binding CloseViewCommand}"></Button>
                        </Grid>
                        <Border BorderBrush="Black"
                                BorderThickness="2"
                                Margin="0"
                                VerticalAlignment="Center"
                                HorizontalAlignment="Center"
                                Grid.Row="1">
                            <tk:Viewbox Stretch="Uniform"
                                        StretchDirection="Both">
                                <ContentControl Content="{Binding PluginView}">  </ContentControl>
                            </tk:Viewbox>
                        </Border>
                        <Thumb x:Name="SizeGrip"
                               Grid.Row="2"
                               VerticalAlignment="Bottom"
                               HorizontalAlignment="Right"
                               Width="10"
                               Height="10"
                               Margin="0,0,-7,-7"
                               Style="{StaticResource SizeGrip}"
                               DragDelta="SizeGrip_DragDelta" />
                    </Grid>
                </Border>
            </DataTemplate>                
        </ListBox.ItemTemplate>
    </ListBox>

This "WorkspaceView" has a ViewModel that has a Plugins collection (IWorkspacePlugin) that the ListBox ItemsSource is bound to. All of this is done so that I can display a window around each plugin that has an "X" close button which should be bound to the IWorkspacePlugin's "ClosePluginCommand" property. The problem I am having is that I don't know how to get the plugin's view (IWorkspacePlugin.PluginView) injected where the ContentControl is in the ListBox.ItemTemplate through the binding "PluginView". How do I do that? Am I going about this all the wrong way?

Any guidance would be appreciated, thank you!

Dec 16, 2009 at 11:40 PM

One way of doing this (albeit with MEF/MVVM/WPF) is outlined in this article. I'm not sure ResourceDictionary Importing is relvant to Prism/Silverlight (I don't use either), but maybe its worth taking a look.

The magic happens mainly in the sample's app.xaml.cs as well as by defining his "Views" each as a ResourceDictionary that contain one DataTemplate with the type set to the relevant ViewModel. All of this comes together via when the composition step is finished (after which OnImportsSatisfied() gets called and the Imported ResourceDictionary objects each get added to the app's Resources).

GL!

Dec 17, 2009 at 7:35 PM

I have made some progress on this issue but I hit another road block along the way after thinking I had it solved. Here is where I'm at now:

The plugins are registered with a WorkSpaceManager class during their particular PRISM IModule.Initialize() methods like so:

workspace.RegisterPlugin(new PluginInfo() { Name = "MyPlugin", ViewType = typeof(MyPluginView), SettingsViewType = null });

The RegisterPlugin() method simply adds the PluginInfo object to a dictionary keyed on the "Name" property. Then when I want to add a plugin to the workspace I do the following:

workspace.AddPluginToWorkspace("MyPlugin");

The AddPluginToWorkspace method of the WorkspaceManager class looks like this:

public void AddPluginToWorkspace(string pluginName)
    {
        if (AvailablePlugins.ContainsKey(pluginName))
        {
            PluginInfo pi = AvailablePlugins[pluginName];
            WorkspacePlugin wsp = new WorkspacePlugin();

            // Create the View
            wsp.View = (Control)this.unityContainer.Resolve(pi.ViewType);
            wsp.Name = pi.Name;

            // Wire up the CloseCommand to WorkspaceManager's PluginClosing handler
            wsp.CloseCommand = new DelegateCommand<WorkspacePlugin>(this.PluginClosing);

            // Add the plugin to the active plugins (modules) collection
            this.modules.Add(wsp);

            // FIX: This should notify anyone listening that the ActivePlugins have changed. When enabled, this causes the same error that will be mentioned further on when attempting to close a plugin.
            //this.eventAggregator.GetEvent<ActivePluginsChanged>().Publish(wsp);
        }

    }

The Workspace ViewModel simply exposes the WorkspaceManager Service's modules collection which is the datacontext of the Workspace View as shown here:

<Grid x:Name="LayoutRoot"
      Background="White">
    <ListBox x:Name="ModuleListBox"
             Grid.Row="1"
             rgn:RegionManager.RegionName="Workspace"
             Background="Yellow"
             ItemsSource="{Binding Plugins}">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
        <ListBox.Template>
            <ControlTemplate>
                <Grid x:Name="ListBoxGrid">
                    <ItemsPresenter></ItemsPresenter>
                </Grid>
            </ControlTemplate>
        </ListBox.Template>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Border BorderBrush="Black"
                        BorderThickness="2"
                        Margin="0"
                        Padding="0">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="20"></RowDefinition>
                            <RowDefinition Height="*"></RowDefinition>
                            <RowDefinition Height="5"></RowDefinition>
                        </Grid.RowDefinitions>
                        <Grid Grid.Row="0">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"></ColumnDefinition>
                                <ColumnDefinition Width=".05*"></ColumnDefinition>
                            </Grid.ColumnDefinitions>
                            <Button Content="X"
                                    HorizontalAlignment="Right"
                                    Grid.Column="1"
                                    cmd:Click.Command="{Binding CloseCommand}"
                                    cmd:Click.CommandParameter="{Binding}"></Button>
                        </Grid>
                        <Border BorderBrush="Black"
                                BorderThickness="2"
                                Margin="0"
                                VerticalAlignment="Center"
                                HorizontalAlignment="Center"
                                Grid.Row="1">
                            <tk:Viewbox Stretch="Uniform"
                                        StretchDirection="Both">
                                <ContentControl Content="{Binding View}"></ContentControl>                    
                            </tk:Viewbox>
                        </Border>                            
                    </Grid>
                </Border>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

Notice the Content control that is bound to the "View" property of the WorkspacePlugin and the Button that has the Click.Command bound to the "CloseCommand". This is where I was stuck initially but for the most part, this works. The plugin's view is loaded inside the other controls and I'm still able to bind the close command (And other commands to be added at a later time) to an underlying model.

The problem now is that whenever I click the close button and the WorkspacePlugin is removed from the modules collection, a property changed event is fired on the ViewModel to let the listbox know to update I get the following error (This also happens if I uncomment the line below the "FIX" comment above:

System.ArgumentException: Value does not fall within the expected range. at MS.Internal.XcpImports.CheckHResult(UInt32 hr) at MS.Internal.XcpImports.SetValue(INativeCoreTypeWrapper obj, DependencyProperty property, DependencyObject doh) at MS.Internal.XcpImports.SetValue(INativeCoreTypeWrapper doh, DependencyProperty property, Object obj) at System.Windows.DependencyObject.SetObjectValueToCore(DependencyProperty dp, Object value) at System.Windows.DependencyObject.RefreshExpression(DependencyProperty dp) at System.Windows.Data.BindingExpression.RefreshExpression() at System.Windows.Data.BindingExpression.SendDataToTarget() at System.Windows.Data.BindingExpression.SourceAquired() at System.Windows.Data.BindingExpression.DataContextChanged(Object o, DataContextChangedEventArgs e) at System.Windows.FrameworkElement.OnDataContextChanged(DataContextChangedEventArgs e) at System.Windows.FrameworkElement.OnTreeParentUpdated(DependencyObject newParent, Boolean bIsNewParentAlive) at System.Windows.DependencyObject.UpdateTreeParent(IManagedPeer oldParent, IManagedPeer newParent, Boolean bIsNewParentAlive, Boolean keepReferenceToParent) at MS.Internal.FrameworkCallbacks.ManagedPeerTreeUpdate(IntPtr oldParentElement, IntPtr parentElement, IntPtr childElement, Byte bIsParentAlive, Byte bKeepReferenceToParent)

From what I gather by looking online, this typically means that a visual element that was already added to the visual tree is trying to be added again. This kind of makes since if I only have 1 plugin displayed and close it, it disappears and there is no error. I am fairly certain that the error is due to the WorkspacePlugin.View property being a visual control and the binding update is attempting to re-add it to the visual tree.

How can I work around this or achieve the desired result without the error message?