Part I: Introducing NHiberate and WPF: The ChinookMediaManager
Part II: Nhibernate and WPF: The core
Part III: Nhibernate and WPF: Models concept
In this post I will introduce some concepts about the presentation layer of the Chinook Media Manager example.
If you are new to the MVVM pattern I will strongly recommend you these blogs post:
I will quote John Smith:
The single most important aspect of WPF that makes MVVM a great pattern to use is the data binding infrastructure. By binding properties of a view to a ViewModel, you get loose coupling between the two and entirely remove the need for writing code in a ViewModel that directly updates a view.
This is the more important thing that you need to remember. Unlike the MVP pattern the viewmodel never updates the UI, the ui is automatically updated by the databinding infrastructure. Also you has to keep in mind that all in the MVVM is about databinding, even events.
I chose to separate Views and ViewModels in differents assembly, although I saw these two together in a bunch of samples. For the other hand the interfaces of the ViewModels are defined in the Views assembly.
If we recall my first post the UI was this:
I will separate this use case in two views and viewmodels:
The main View is called AlbumManagerView and the view inside the red border is called “EditAlbumView”. The reason for why I’m doing this is clear: “I need somehow to separate the problem into smaller parts”. And as I say in the “Models concept” post a use-case can expand multiples views.
To complicate things a bit, the user told us that would like to be able to simultaneously edit multiple albums. So, I will use the workspace sample of John Smith.
When I start to define a View, the first thing is the ViewModel interface, in this case:
public interface IAlbumManagerViewModel : INotifyPropertyChanged { /// <summary> /// Setup the view, load the albums collection. /// </summary> /// <param name="artist"></param> void SetUp(Artist artist); /// <summary> /// Expose a bindable collection of albums. /// </summary> IEnumerable<Album> Albums { get; } /// <summary> /// Get or Set the selected album. /// </summary> Album SelectedAlbum { get; set; } /// <summary> /// Open an edition workspace for editing the selected album. /// </summary> ICommand EditSelectedAlbumCommand { get; } /// <summary> /// Commit all the changes. /// </summary> ICommand SaveAllCommand { get; } /// <summary> /// Discard all the changes. /// </summary> ICommand CancelAllCommand { get; } /// <summary> /// WorkSpace open. /// </summary> ObservableCollection<IEditAlbumViewModel> AlbumEditWorkspaces { get; } }
In the introduction I said that MVVM use databinding even for events .
So, what is an ICommand? ICommand interface has three members: CanExecuteChanged, CanExecute and Execute. The object that is bound to this command (aka command source), disable itself if the command cann’t be executed. You can bind KeyGestures, MouseActions, buttons and so on. More on this topic here.
The second step is to start writing a test for the concrete implementation of the viewmodel.
[TestFixture] public class AlbumManagerViewModelTest { [Test] public void setup_viewmodel_should_work() { var albumManagerModel = new Mock<IAlbumManagerModel>(); var artist = new Artist { Name = "John" }; var albumList = new List<Album> { new Album() { Artist = artist } }; albumManagerModel.Setup(am => am.GetAlbumsByArtist(artist)) .Returns(albumList) .AtMostOnce(); var albumManagerVm = new AlbumManagerViewModel(albumManagerModel.Object, viewInsantiator.Object); var eventWasRaised = false; albumManagerVm.PropertyChanged += (sender, args) => { //property changed should be raised AFTER the property change. if ("Albums".Equals(args.PropertyName)) { albumManagerVm.Albums.Should().Be.SameInstanceAs(albumList); eventWasRaised = true; } }; albumManagerVm.SetUp(artist); eventWasRaised.Should().Be.True(); albumManagerModel.VerifyAll(); }
When I setup an “AlbumManagerViewModel”, it should call GetAlbumByArtist of my model, and put the result in the “Albums” property. Also the viewmodel should raise the PropertyChanged for the album property, AFTER the change in the property.
The implementation is very simple:
public void SetUp(Artist artist) { Albums = _albumManagerModel.GetAlbumsByArtist(artist); }
private IEnumerable<Album> _albums; public IEnumerable<Album> Albums { get { return _albums; } private set { _albums = value; OnPropertyChanged("Albums"); } }
To not getting bored with the code, the full implementation of the EditSelectedAlbumCommand is here.
I will highlight interesting parts of this code:
public ICommand EditSelectedAlbumCommand { get { if (_editSelectedAlbumCommand == null) _editSelectedAlbumCommand = new RelayCommand( o => EditSelectedAlbum(), o => SelectedAlbum != null &&!AlbumEditWorkspaces.Any(ae => ae.Album == SelectedAlbum)); return _editSelectedAlbumCommand; } }
This is the EditSelectedAlbumCommand. In the instantiation of the command you see two lambdas. The first function is the code that should be called when the commandsource call Execute. The second is the CanExecute function, as you can see it returns false when:
You can find the RelayCommand in the source.
The EditSelectedAlbum method looks as follows:
private void EditSelectedAlbum() { //Resolve a new instance of EditAlbumViewModel var newWp = _viewFactory.ResolveViewModel<IEditAlbumViewModel>(); //Setup the new viewmodel. newWp.SetUp(SelectedAlbum, _albumManagerModel); //subscribe to close request. newWp.RequestClose += EditAlbumRequestClose; //add the new viewmodel to the workspace collection. AlbumEditWorkspaces.Add(newWp); //set the new viewmodel as the active wp. SetActiveWorkspace(newWp); }
Now, the view for this viewmodel is very easy:
<StackPanel> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" > <Button Command="{Binding EditSelectedAlbumCommand}" >Edit</Button> <Button>Close</Button> </StackPanel> <ListBox Name="AlbumsList" ItemsSource="{Binding Albums}" DisplayMemberPath="Title" SelectedItem="{Binding SelectedAlbum}" DockPanel.Dock="Top" Height="302"> </ListBox> <Button Command="{Binding SaveAllCommand}" >Save All</Button> <Button Command="{Binding CancelAllCommand}" >Cancel All</Button> </StackPanel>
The “tabs” container is described in this peace of code:
<Border Style="{StaticResource MainBorderStyle}"> <HeaderedContentControl Content="{Binding Path=AlbumEditWorkspaces}" ContentTemplate="{StaticResource WorkspacesTemplate}" Header="Workspaces" Style="{StaticResource MainHCCStyle}" /> </Border>
I define the interface for the IEditAlbumViewModel as follows:
public interface IEditAlbumViewModel : INotifyPropertyChanged { void SetUp(Album album, IAlbumManagerModel albumManagerModel); /// <summary> /// The album being edited. /// </summary> Album Album { get; } /// <summary> /// The title of the tab. /// </summary> string DisplayName { get; } /// <summary> /// The command to close the tab. /// </summary> ICommand CloseCommand { get; } /// <summary> /// The command to save the album. /// </summary> ICommand SaveCommand { get; } /// <summary> /// The command to add a new track to the album. /// </summary> ICommand AddNewTrackCommand { get; } /// <summary> /// The command to delete the selected track. /// </summary> ICommand DeleteSelectedTrackCommand { get; } Track SelectedTrack { get; set; } event EventHandler RequestClose; }
The implementation is very straightforward, you can see here. One thing to mention is that I configure nhibernate to resolve all my collections with INotifyCollectionChanged, so I can add a new Track in my viewmodel and the datagrid bound to the tracks collection will be notified about that change.
We have several troubles with this application:
Here you can see the full concept of "Conversation per Business Transaction" of Fabio Maulo working.
The full source code project is here.
Special thanks to all the people mentioned in this post.