MVP In Silverlight/WPF: Implementing The Details UserControl

5 commentsWritten on August 5th, 2010 by
Categories: MVP In Silverlight/WPF

Note: This post is part of a series. You can find the introduction and overview of the series here.

The second (and last) UserControl of this series and its accompanying sample looks like this:

Nothing fancy (it never is when i do the UI) and pretty much a typical edit screen, though there are very few fields to edit obviously. The DropDown shows the suitable parent User Groups for this User Group. These suitable parents are retrieved from the Service Layer and the 'logic' behind them is very simple: it can't be the selected User Group, and it can't be any User Group that is currently below it in the hierarchy. Other than that, anything goes.

The 3 buttons should be self-explanatory as well. If you press the Cancel button, the TextBox and the DropDown should be reset to their initial values, which are either empty values in case the user is creating a new User Group, or the original values of the currently selected User Group in our previous UserControl's TreeView. If you press the Delete button, the currently selected User Group needs to be deleted. The Delete button can obviously only be shown in case we're editing an existing User Group, and never if we're creating a new one since that wouldn't make sense. The Save button persists the changes, which means either updating the currently selected User Group or inserting the newly created one. The Delete button can't be shown when the user does not have the required permission to delete a User Group, and the Save button can't be shown if the user doesn't have the required permission to edit a User Group. However, if the user does have permission to create a new User Group (which is a separate permission from editing an existing one) then the Save button must be visible.

Alright, let's get started. I'm going to use a different style in this post than i used in the last one though. The last post was more of a step-by-step walk through of writing the actual code, but that leads to very long posts (and takes up a lot more of my time to write it), and i'd like to keep this one a bit shorter. So i'm just going to show the entire code of each class with some comments on it.

As usual i like to start off with the BindingModel. What exactly are we going to put into it? We'll obviously need some stuff from the User Group: its ID (though we won't display that), name and the parent User Group (if there is one). We'll also need a list of suitable parents. Remember that we also need to support the Cancel button, so we need to store the original values. This is what i came up with:

    public class UserGroupDetailBindingModel : BindingModel<UserGroupDetailBindingModel>
    {
        private string originalName;
        private Guid? originalId;
        private UserGroupDto originalSelectedParent;
 
        public ObservableCollection<UserGroupDto> SuitableParentUserGroups { get; private set; }
 
        private UserGroupDto selectedParentUserGroup;
 
        public UserGroupDto SelectedParentUserGroup
        {
            get { return selectedParentUserGroup; }
            set
            {
                selectedParentUserGroup = value;
                NotifyPropertyChanged(m => m.SelectedParentUserGroup);
            }
        }
 
        private Guid? id;
 
        public Guid? Id
        {
            get { return id; }
            set
            {
                id = value;
                NotifyPropertyChanged(m => m.Id);
                NotifyPropertyChanged(m => m.IsExistingUserGroup);
            }
        }
 
        public bool IsExistingUserGroup { get { return id.HasValue && id.Value != Guid.Empty; } }
 
        private string name;
 
        public string Name
        {
            get { return name; }
            set
            {
                name = value;
                NotifyPropertyChanged(m => m.Name);
            }
        }
 
        public UserGroupDetailBindingModel()
        {
            SuitableParentUserGroups = new ObservableCollection<UserGroupDto>();
            Clear();
            AddValidationFor(m => m.Name)
                .When(m => string.IsNullOrWhiteSpace(m.name))
                .WithMessage("name is a required field");
        }
 
        public void Clear()
        {
            SuitableParentUserGroups.Clear();
            SuitableParentUserGroups.Add(new UserGroupDto { Id = Guid.Empty, Name = "None" });
            SelectedParentUserGroup = SuitableParentUserGroups[0];
 
            originalId = Id = null;
            originalName = Name = null;
            originalSelectedParent = SelectedParentUserGroup;
        }
 
        public void Populate(IEnumerable<UserGroupDto> suitableParentUserGroups, UserGroupDto currentUserGroup = null)
        {
            foreach (var suitableParentUserGroup in suitableParentUserGroups)
            {
                SuitableParentUserGroups.Add(suitableParentUserGroup);
            }
 
            if (currentUserGroup != null)
            {
                originalName = Name = currentUserGroup.Name;
                originalId = Id = currentUserGroup.Id;
                originalSelectedParent = SelectedParentUserGroup;
 
                if (currentUserGroup.ParentId.HasValue)
                {
                    originalSelectedParent = SelectedParentUserGroup = SuitableParentUserGroups.First(u => u.Id == currentUserGroup.ParentId);
                }
            }
        }
 
        public void RevertToOriginalValues()
        {
            Name = originalName;
            Id = originalId;
            SelectedParentUserGroup = originalSelectedParent;
        }
    }

Looking back on this now, there are a couple of things that i don't really use. For one, the ID property raises the PropertyChanged event even though there's nothing that binds to it. I also have an IsExistingUserGroup property but i don't use it anywhere. Unfortunately, i only noticed this after releasing the sample so i'm not just gonna go back and change it now. Probably just a brainfart on my part. Anyways, the only interesting parts to note about this BindingModel is the simple validation that we define on the Name property (and which was actually already discussed in the post about the infrastructure bits) and the fact that we add a default 'empty' User Group to the SuitableParentUserGroups collection. Other than that, everything here should be very clear and straightforward by now so let's just move on to the presenter already:

    public class UserGroupDetailPresenter : Presenter<IUserGroupDetailsView, UserGroupDetailBindingModel>,
        IListenTo<UserGroupSelectedEvent>, IListenTo<UserGroupNeedsToBeCreatedEvent>
    {
        public UserGroupDetailPresenter(IUserGroupDetailsView view, IEventAggregator eventAggregator, IAsyncRequestDispatcherFactory requestDispatcherFactory)
            : base(view, eventAggregator, requestDispatcherFactory) {}
 
        public override void Initialize()
        {
            View.Hide();
            EventAggregator.Subscribe(this);
        }
 
        public void Handle(UserGroupNeedsToBeCreatedEvent receivedEvent)
        {
            View.PreventDeletion();
            LoadData();
        }
 
        public void Handle(UserGroupSelectedEvent receivedEvent)
        {
            View.EnableEverything();
            LoadData(receivedEvent.SelectedUserGroupId);
        }
 
        private void LoadData(Guid? userGroupId = null)
        {
            BindingModel.Clear();
 
            var requestDispatcher = RequestDispatcherFactory.CreateAsyncRequestDispatcher();
 
            if (userGroupId.HasValue)
            {
                requestDispatcher.Add(new CheckPermissionsRequest {PermissionsToCheck = new[] {Permissions.DeleteUserGroup, Permissions.EditUserGroup}});
                requestDispatcher.Add(new GetUserGroupRequest { UserGroupId = userGroupId.Value });
            }
            requestDispatcher.Add(new GetSuitableParentUserGroupsRequest {UserGroupId = userGroupId});
            requestDispatcher.ProcessRequests(ResponsesReceived, PublishRemoteException);
        }
 
        private void ResponsesReceived(ReceivedResponses receivedResponses)
        {
            if (receivedResponses.HasResponse<GetUserGroupResponse>())
            {
                BindingModel.Populate(receivedResponses.Get<GetSuitableParentUserGroupsResponse>().SuitableParentUserGroups,
                    receivedResponses.Get<GetUserGroupResponse>().UserGroup);
            }
            else
            {
                BindingModel.Populate(receivedResponses.Get<GetSuitableParentUserGroupsResponse>().SuitableParentUserGroups);
            }
 
            if (receivedResponses.HasResponse<CheckPermissionsResponse>())
            {
                var response = receivedResponses.Get<CheckPermissionsResponse>();
                if (!response.AuthorizationResults[Permissions.DeleteUserGroup]) View.PreventDeletion();
                if (!response.AuthorizationResults[Permissions.EditUserGroup]) View.PreventModification();
            }
 
            View.Show();
        }
 
        public void PersistChanges()
        {
            BindingModel.ValidateAll();
            if (BindingModel.HasErrors) return;
 
            var dispatcher = RequestDispatcherFactory.CreateAsyncRequestDispatcher();
            dispatcher.Add(new SaveUserGroupRequest
            {
                Id = BindingModel.Id,
                Name = BindingModel.Name,
                ParentId = BindingModel.SelectedParentUserGroup.Id != Guid.Empty ? BindingModel.SelectedParentUserGroup.Id : (Guid?)null
            });
            dispatcher.ProcessRequests(PersistChanges_ResponseReceived, PublishRemoteException);
        }
 
        private void PersistChanges_ResponseReceived(ReceivedResponses responses)
        {
            var response = responses.Get<SaveUserGroupResponse>();
 
            if (response.NewUserGroupId.HasValue)
            {
                BindingModel.Id = response.NewUserGroupId.Value;
            }
 
            EventAggregator.Publish(new UserGroupChangedEvent
            {
                Id = BindingModel.Id.Value,
                Name = BindingModel.Name,
                ParentId = BindingModel.SelectedParentUserGroup.Id != Guid.Empty ? BindingModel.SelectedParentUserGroup.Id : (Guid?)null,
                IsNew = response.NewUserGroupId.HasValue
            });
        }
 
        public void Delete()
        {
            var dispatcher = RequestDispatcherFactory.CreateAsyncRequestDispatcher();
            dispatcher.Add(new DeleteUserGroupRequest { UserGroupId = BindingModel.Id.Value });
            dispatcher.ProcessRequests(DeleteUserGroup_ResponseReceived, PublishRemoteException);
        }
 
        private void DeleteUserGroup_ResponseReceived(ReceivedResponses responses)
        {
            EventAggregator.Publish(new UserGroupDeletedEvent(BindingModel.Id.Value));
        }
 
        public void Cancel()
        {
            BindingModel.RevertToOriginalValues();
        }
    }

As you can see, this presenter doesn't retrieve any data in its Initialize method. In fact, it just hides the View and subscribes with the Event Aggregator. This UserControl only needs to be visible once the user has selected a User Group in the Overview UserControl, so the View remains hidden until we actually need to show something.

In the Handle(UserGroupNeedsToBeCreatedEvent) method, we first instruct the View to prevent the user from pressing the Delete button (since that wouldn't make sense during the creation of a new User Group) and we call the LoadData method. The Handle(UserGroupSelectedEvent) method first instructs the view to enable everything (all controls basically) and then calls the LoadData method with the ID of the currently selected User Group. If the userGroupId parameter is passed into the LoadData method, we'll not only retrieve the suitable parents, but also the details of the current User Group, as well as check whether our user has permission to delete and/or edit a User Group. And obviously, being the responsible programmers that we are, we send all 3 requests in the same roundtrip since there is no reason whatsoever not to do so.

In the ResponsesReceived method, we populate the model based on the data we've received from the Service Layer. We also tell the View to prevent deletion of the current User Group if the user doesn't have permisson to do so, and we also tell the View to prevent modification if necessary. Finally, we tell the View to show itself to the user.

The PersistChanges method is the one that will be called by the View when the Save button is clicked. If the BindingModel has validation errors, we simply return from the method. Since we use the INotifyDataErrorInfo interface in our BindingModel (as discussed in the Infrastructure Bits post), the View will automatically show the validation message anyway and we don't need to do anything. We could have also bound the Visibility property of the Save button to the HasErrors property of the BindingModel to prevent it from being visible as long as there are validation problems, but then we'd also need to keep the permissions into account. You could do it in various ways, and i just didn't go through the extra effort of actually doing so since this is after all just a silly sample. Anyways, if there are no validation errors, we send a request to the Service Layer to save the User Group's data.

In the PersistChanges_ResponsesReceived method, we update the Id property of the BindingModel if necessary, and we publish a UserGroupChangedEvent. As you've seen in the last post, that event will be handled by the Overview UserControl so it can update its TreeView. As you can see, the Delete method is pretty similar, so there's no need to explain it. And finally, the Cancel method simply calls the RevertToOriginalValues method on the BindingModel.

Now that we have our BindingModel and our Presenter, we can start working on our View. The XAML looks like this (again, i suck at XAML so this is probaby far from good XAML... if there is such a thing, that is):

<MVP:View x:Class="SilverlightMVP.Client.Views.UserGroupDetail"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:MVP="clr-namespace:SilverlightMVP.Client.Infrastructure.MVP" >
 
    <Grid x:Name="LayoutRoot" Background="White" MinHeight="75" MaxHeight="75" MinWidth="455" >
 
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
 
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
 
        <TextBlock Text="Name" Grid.Column="0" Grid.Row="0" />
        <TextBox x:Name="NameTextBox" Text="{Binding Path=Name, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
                Grid.Column="1" Grid.Row="0" Grid.ColumnSpan="2" />
 
        <TextBlock Text="Parent" Grid.Column="0" Grid.Row="1" />
        <ComboBox x:Name="SuitableParentUserGroupsComboBox" ItemsSource="{Binding Path=SuitableParentUserGroups}" MinWidth="150"
                 DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedParentUserGroup, Mode=TwoWay}"
                 Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="2" />
 
        <Button x:Name="DeleteButton" Content="Delete" Click="DeleteButton_Click" Grid.Column="0" Grid.Row="2" />
        <Button x:Name="CancelButton" Content="Cancel" Click="CancelButton_Click" Grid.Column="1" Grid.Row="2" />
        <Button x:Name="SaveButton" Content="Save" Click="SaveButton_Click" Grid.Column="2" Grid.Row="2" />
 
    </Grid>
</MVP:View>

And the View's code would be this:

    public interface IUserGroupDetailsView : IView
    {
        void PreventDeletion();
        void PreventModification();
        void EnableEverything();
    }
 
    public partial class UserGroupDetail : IUserGroupDetailsView
    {
        private readonly UserGroupDetailPresenter presenter;
 
        public UserGroupDetail()
        {
            InitializeComponent();
            presenter = CreateAndInitializePresenter<UserGroupDetailPresenter>();
        }
 
        public void EnableEverything()
        {
            DeleteButton.Visibility = Visibility.Visible;
            CancelButton.Visibility = Visibility.Visible;
            SaveButton.Visibility = Visibility.Visible;
            NameTextBox.IsEnabled = true;
            SuitableParentUserGroupsComboBox.IsEnabled = true;
        }
 
        public void PreventDeletion()
        {
            DeleteButton.Visibility = Visibility.Collapsed;
        }
 
        public void PreventModification()
        {
            NameTextBox.IsEnabled = false;
            SuitableParentUserGroupsComboBox.IsEnabled = false;
            CancelButton.Visibility = Visibility.Collapsed;
            SaveButton.Visibility = Visibility.Collapsed;
        }
 
        private void DeleteButton_Click(object sender, RoutedEventArgs e)
        {
            presenter.Delete();
        }
 
        private void CancelButton_Click(object sender, RoutedEventArgs e)
        {
            presenter.Cancel();
        }
 
        private void SaveButton_Click(object sender, RoutedEventArgs e)
        {
            presenter.PersistChanges();
        }
    }

And that's it.

I apologize to those of you who prefer the style of the previous post, but i'm sort of behind schedule and won't be able to write anything for the next 4 days, so i'm trying to get ahead enough of the posting schedule ;) . Though i hope you'll agree that the walk-through style of the previous post wasn't necessary anymore after going through a full implementation once.

Anyways, in the next post of the series, we'll look into the automated tests of both the BindingModel and the Presenters.

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #659

  • http://Www.online.no/~ronno Pedro Dias

    I am faiing to see how this is preferred over MVVM??

    I actually had to read your example several times before I had the entire picture, and the differences I see are that you have your service class called in the presenter, which us aliens call “viewmodel”.

    The presenter, view, as wel, as the model all contain logic! Adding insult to injury, you have function names that do MORE than the name suggests, and from where I am sitting, I am looking at method names that only add to confusion instead of removing any doubt.

    You also have a whole bunch of logic going on in your view codebehind. That is a fail among my devs, that they have to redo whenever I catch them trying.

    In short, you’re not selling MVP with that example

  • http://davybrion.com Davy Brion

    @Pedro

    it depends on your preferences

    from what you’re saying, you want _everything_ to be done in your ViewModel… If you want all logic to be located there and feel fine with that, then by all means go ahead and do so.

    “You also have a whole bunch of logic going on in your view codebehind. That is a fail among my devs, that they have to redo whenever I catch them trying.”

    Your devs, your rules. Over here we consider putting all logic in the same place a fail, but hey, that’s just us

    As for the code in the View, please give me some _real_ reasons as to why that kind of code shouldn’t be put there?

    Did you read the rest of the series btw? or are you just skipping through?

  • bennyb

    @Pedro Dias
    One of the main selling points of MVP is that it helps you stay focussed on each individual section of your code.
    With MVVM, all those button clicks for example, will have been ViewModel commands thus obfuscating the major responsibility of the model (data properties)

  • Drew

    Most of the validation logic should stay in the Model class, as the validation logic is really business logic.  However, there is no clean, easy way to pull the validation logic from the Model and apply it to the BindingModel.  Any thoughts or ideas on this?