Panorama Hierarchy and AppBarButtons

Apr 27, 2011 at 2:42 PM

Ok so i'm trying to reduce a complex hierarchy that is basically a panorama control where each panorama item has a list of items.  See this post for the structure http://caliburnmicro.codeplex.com/discussions/254980

So i have a view fore the panorama, a view fore the panorama item, a view fore the list and a view for each item in the list.

the bindings all work but what i'm not really sure is how can the appbarbutton located at the root of the visual tree delete an item from the list in the selected panoramaitem?

Apr 27, 2011 at 10:38 PM

Modifying your model as:

	public class Library : PropertyChangedBase
    {
        public Library()
        {
            Categories = new ObservableCollection<Category>();
            Categories.Add(new Category() { Title = "Science" });
            Categories.Add(new Category() { Title = "Fiction" });
        }
        
        public ObservableCollection<Category> Categories { get; set; }
        
        Category _SelectedCategory ;
        public Category SelectedCategory { 
			get {return _SelectedCategory; } 
			set {_SelectedCategory = value; NotifyOfPropertyChange(()=>SelectedCategory); }
		} 
		
		private Book GetSelectedBook() {
			if (SelectedCategory == null) return null;
			return SelectedCategory.SelectedBook;
		}
		
    }

    public class Category : PropertyChangedBase
    {
        public Category()
        {
            Books = new ObservableCollection<Book>();
            Books.Add(new Book() { Name = "Some Book", Description = "Once apon a time.." });
        }
        public string Title { get; set; }
        public ObservableCollection<Book> Books { get; set; }
        
        Book _SelectedBook;
        public Category SelectedBook  { 
			get {return _SelectedBook ; } 
			set {_SelectedBook  = value; NotifyOfPropertyChange(()=>SelectedBook ); }
		} 
        
    }
    public class Book : PropertyChangedBase
    {
        public string Name { get; set; }
        public string Description { get; set; }
    }

and configuring the Xaml bindings along this line:

<controls:Panorama 
	ItemsSource="{Binding Path=Library.Categories}" 
	SelectedItem="{Binding Path=Library.SelectedCategory, Mode=TwoWay}" 
	
	... >
		...
			
			<ListBox 
				ItemsSource="{Binding Path=Books}"
				SelectedItem="{Binding Path=SelectedBook, Mode=TwoWay}" 
				... >
			
				...
				
			</ListBox>
		...
</controls:Panorama>

you have the new properties SelectedCategory and SelectedBook always synchronized with the current state of the UI.
As a consequence you can access the currently selected book in the currently selected category from the root library using the code in the "GetSelectedBook" sample function.

Apr 28, 2011 at 12:22 AM

Sorry the structure has actually changed a lot since then, what i was trying to get at is just the Hierarchy now i've broken the panorama item into multiple views and viewmodels. I've uploaded the project if you want to take a look

http://cid-45301901ee7e4a69.office.live.com/self.aspx/Caliburn/LibrarySample%20With%20Screens.zip

Apr 28, 2011 at 6:18 PM

I'm having a look to the project. It has broken reference, so I've to get it squared first.
I'll keep you posted up here.

Apr 28, 2011 at 7:11 PM

ah sorry thats my infrastructure project thats shared so its not in the standard solution location. heres the dll  

http://cid-45301901ee7e4a69.office.live.com/self.aspx/Caliburn/Praethin.Infrastructure.zip

Apr 28, 2011 at 10:48 PM

There was several problems preventing things to work; also, there was some confusion around the shape of the view models, perhaps due to your attempts.

- Even if the model has changed, my original advice is still valid. The only difference is that the view is now (correclty) splitted into smaller ones.
The important points of my sample are the SelectedXXX properties (with the relative change notification) and the bi-directional binding against the SelectedItem properties.
The Panorama and the ListBox have (from a "semantic" point of view) the same behavior: both represents a set of items, one of which is "selected" (active). As a matter of fact, they both inherits from ItemsControl.
You basically just have to bind the ItemSource property to an observable collection in the VM to populate the list, and the SelectedItem property to a single property in the VM representing the active item.
Every time the user changes the currently active item, this change is reflected in the VM through the bi-directional binding; the same goes in backward direction: setting the property in the VM changes the selection in the view.

- You can avoid explicit bindings leveraging CM conventions; if you have an ItemsControl named Books, CM binds the ItemsSource property of the control to the Books property of the VM and the SelectedItem property to the SelectedBook.

- Inheriting your models from Conductor<>.Collection.OneActive is useful (other than to enforce the lifecycle of conducted screens) to avoid writing a tree-shaped VM. For example, the LibraryVM could be rewritten as:

public class LibraryViewModel : Conductor<CategoryViewModel>.Collection.OneActive
    {
        public LibraryViewModel()
		{
            this.Items.Add(new CategoryViewModel(BookCategory.Science));
            this.ActivateItem(new CategoryViewModel(BookCategory.Fiction));
		}
    }

This class has the same shape of the original one, with a (typed) collection of CategoryVMs (.Items) and a current one (.ActiveItem). 

- In order to access the selected book, you have to determine the selected category, first, then get the selected book from that one.
In other words, you just have to navigate your composite VM through the SelectedXXX properties.

- In order to invoke an action from the Application Bar, the action should live in the VM bound to the view containing the AppBar. In your scenario, the action should be in the ShellVM.
From there, you can access the currently selected book as I mentioned before.

 

Does it help?

 

Apr 29, 2011 at 3:23 AM

Thanks for taking the time to explain that. I guess it wasn't really what i was expecting. I thought there was some way that binding or action would bubble up or down the UI Tree.  but it looks like you just need to drill into the active items. Something like ActiveItem.ActiveItem.ActiveItem.ActiveItem, being the Shell, Library, Category, Day. To me this seems a little unreadable though. 

Apr 29, 2011 at 4:32 AM

Also if you where to go the screen approach what's the recommended way to design your models. Would you keep them hierarchical and pass it down into the constructor of each screen or would you flatten them and keep a record of the parent object. like a book needs to remember the category (though in my real app the object in place of the category can change)

Apr 29, 2011 at 9:02 AM

Actions actually *do* bubble, but just from leaf to root nodes. For example, if you had a button in the BookView, it could invoke a "DoWhatever" action in the ShellVM.
Your scenario, as I understand it, is different: you have to trigger an action living in the ShellVM from the root of the UI tree (namely, from the AppBar in the ShellView).
The tricky part is that the action has to operate on the currently selected item; in other words, the action has to know about the current "state" (which includes current selections) of the whole VM tree.
This is perfectly fine, and is how MVVM is intended to work.

I agree, however, that the "drill down" into the tree is quite ugly. Using explicitly modeled nodes instead of Conductor<> could improve the readability, but it still violates LoD.
To improve the design you might distribute the responsibility of the current selection knowledge along the whole tree: for example you might define 

interface IHasBookSelection {  Book GetSelectedBook();  }

and implement it appropriately in every node of the tree; this way each node just knows about its immediate neighbour.
Another slightly different option, off the top of my head, is to provide a SetCurrentBook method on the root VM and let each node in the tree to register any change under its competence.

Does it make sense? 

Apr 29, 2011 at 10:12 AM

Yeah it does, thanks for the insight. I think i like the SetCurrentBook approach where the root is listening to a change initialised for the lover level views. It does also allow for you to react to that change somewhere else as well. This approach would be something i haven't really done before but i understand the logic.  I'm thinking raise an event when a book is selected and have the LibraryViewModel listen for it then it can set a SelectedBook property in the LibraryViewModel. Probably need to the same thing for the Category. What do you think?

I guess the models could still stay in the hierarchy and get updated by the ViewModels when something happens.  

Apr 29, 2011 at 11:01 AM

I didn't think to the event solution! It's a very good idea to reduce the coupling between components.
Plus, CM already has its own event aggregator, so it's definitely easy to implement without the need to pass references around.

Apr 29, 2011 at 12:18 PM

I'll have a look into the event aggregator then. thanks for the tip.