views:

474

answers:

2

UPDATED (12/17/2009): Now reflects latest progress I've made.

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".

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?

A: 

You likely shouldn't be using both a Prism Region and also binding views to your ListView via ItemsSource. Generally people pick one or the other. I think you might be seeing some odd behavior because you have both.

I would suggest that your Modules contribute "Plugins" in their initialize method.

public MyModule : IModule
{
     IRegionManager _mgr;
     public MyModule(IRegionManager mgr)
     {
          _mgr = mgr;
     }

     public void Initialize()
     {
          _mgr.RegisterViewWithRegion("Workspace", typeof(MyPlugin));
     }

}

You should be able to call it a day after this. You shouldn't have to collect and provide a collection of plugins from your shell to the region you want to show them in... you should let your modules contribute them to the region themselves. This will allow you to keep up a certain amount of abstraction and let your modules be more autonomous.

Good luck.

Anderson Imes
This is how I had things initially but I ran into a couple issues. 1. I thought it was better practice to bind to a ViewModel2. I need to surround each plugin with some ui elements (a border, a resizing handle, and a window title bar with a close button) that can have the button bound to a viewmodel command as well as a second button to bind to a command to display the settings window. I am not sure how to do this.
Tom
AH... that I can help you with. Redo the code for your question as simply as possible and with no binding (this is not necessary and redundant) and I will lead you the rest of the way into customizing your templates. Posting a link to your code would be even more helpful.
Anderson Imes
Please read the question again as I have updated it with my progress in resolving the issue. I partially resolved it but in doing so ran into a road block. Thanks for your help.
Tom
A: 

I ended up getting this working doing the following:

I created a WorkspaceItemView view and ViewModel which look roughly like this:

<UserControl>
<Grid  x:Name="ResizeGrid"
       MouseEnter="Plugin_MouseEnter"
       MouseLeave="Plugin_MouseLeave">
    <Grid.RowDefinitions>
        <RowDefinition Height="20" />
        <RowDefinition Height="*" />
        <RowDefinition Height="5" />
    </Grid.RowDefinitions>

    <Border x:Name="border"
            BorderBrush="Black"
            BorderThickness="2"
            Padding="0"
            Margin="-1,-1,-1,-1">
    </Border>

    <Grid x:Name="grid"
          Grid.Row="0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width=".05*" />
        </Grid.ColumnDefinitions>
        <Thumb HorizontalAlignment="Stretch"
               Background="Green"
               DragDelta="Thumb_DragDelta"
               />
        <Button Content="X"
                HorizontalAlignment="Right"
                Grid.Column="1"
                cmd:Click.Command="{Binding CloseCommand}"
                cmd:Click.CommandParameter="{Binding PluginID}" />
    </Grid>

    <Border BorderBrush="Black"
            BorderThickness="2"
            Margin="0"
            VerticalAlignment="Center"
            HorizontalAlignment="Center"
            Grid.Row="1">
        <tk:Viewbox Stretch="Uniform"
                    StretchDirection="Both">
            <ContentControl rgn:RegionManager.RegionName="PluginViewRegion" />

        </tk:Viewbox>
    </Border>
    <Thumb x:Name="SizeGrip"
           Grid.Row="1"
           VerticalAlignment="Bottom"
           HorizontalAlignment="Right"
           Width="10"
           Height="10"
           Margin="0,0,-7,-7"
           Style="{StaticResource SizeGrip}"
           DragDelta="SizeGrip_DragDelta"
           DragStarted="SizeGrip_DragStarted"
           DragCompleted="SizeGrip_DragCompleted" />

</Grid>
</UserControl>  

public class WorkspaceItemViewModel : INotifyPropertyChanged
{
    private IWorkspaceManager workspaceManager;
    private IRegionManager regionManager;
    private Guid pluginID;

    public WorkspaceItemViewModel(IWorkspaceManager workspaceManager, IRegionManager regionManager)
    {
        this.workspaceManager = workspaceManager;
        this.regionManager = regionManager;
    }

    public DelegateCommand<object> CloseCommand 
    {
        get
        {
            return workspaceManager.CloseCommand;
        }    
    }

    public DelegateCommand<object> SelectCommand
    {
        get
        {
            return workspaceManager.SelectCommand;
        }
    }

    public object CloseCommandParameter
    {
        get
        {
            return this;
        }
    }

    public Guid PluginID
    {
        get
        {
            return this.pluginID;
        }
        set
        {
            this.pluginID = value;
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs("PluginID"));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

}

The WorkspaceManager code to add a plug-in to the workspace looks like this:

public void AddPluginToWorkspace(string pluginName)
    {
        PluginInfo pi = AvailablePlugins[pluginName];
        WorkspacePlugin wsp = new WorkspacePlugin();
        wsp.Name = pi.Name;
        wsp.CloseCommand = new DelegateCommand<object>(this.PluginClosing);
        wsp.SelectCommand = new DelegateCommand<object>(this.PluginSelected);
        wsp.id = System.Guid.NewGuid();
        this.modules.Add(wsp.id, wsp);

        var view = this.unityContainer.Resolve(pluginWindowType);
        if (view is IWorkspacePlugin)
        {
            var iwsp = view as IWorkspacePlugin;
            if (iwsp != null)
            {
                iwsp.PluginID = wsp.id;
            }
        }
        else
        {
            throw new ArgumentException("Plugin view containers must implement IWorkspacePlugin.");
        }

        var workspaceRegion = regionManager.Regions["Workspace"];
        var pluginRegion = workspaceRegion.Add(view, wsp.id.ToString(), true);
        this.unityContainer.RegisterInstance<IRegionManager>(wsp.id.ToString(), pluginRegion);
        pluginRegion.Regions["PluginViewRegion"].Context = view;
        pluginRegion.Regions["PluginViewRegion"].Add(this.unityContainer.Resolve(pi.ViewType));

        this.eventAggregator.GetEvent<ActivePluginsChanged>().Publish(wsp);

}

This essentially creates a scoped region, adds the WorkspaceItemView to the workspace region and then resolves and adds the view of the actual plugin to the PluginViewRegion of the newly added WorkspaceItemView. I have a little bit of cleanup work to do but I think it works pretty well.

Thanks for all of your help.

Tom