2
votes

I'm new to MVVM and WPF and have been completely hung up on this issue for some time now. I'm trying to display a View (UserControl) based on the SelectedItem in a DataGrid. The UserControl renders with the data set in the constructor, but never updates.

enter image description here

I would really appreciate some insight from someone with experience in this. I tried adding a Mediator via setUpdateCallback and now the first row in the datagrid updates with the values of the other rows I click on, but this obviously isn't what I want, I need those updates to happen in the separate client view outside of the datagrid.

ClientPanel.xaml

<UserControl x:Class="B2BNet.View.ClientPanel" 
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:VM="clr-namespace:B2BNet.ViewModel"
         xmlns:V="clr-namespace:B2BNet.View"
         xmlns:local="clr-namespace:B2BNet"
         xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
         mc:Ignorable="d">
<Grid>
    <Grid.DataContext>
        <VM:ClientPanel/>
    </Grid.DataContext>
    <TextBlock HorizontalAlignment="Left" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Height="43" Width="280" Text="{Binding Title}" FontSize="36" FontFamily="Global Monospace"/>
    <DataGrid AutoGenerateColumns="False" x:Name="dataGrid" HorizontalAlignment="Left" Margin="10,60,0,10" VerticalAlignment="Top" ItemsSource="{Binding Clients}" SelectedItem="{Binding currentClient}">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Name" Binding="{Binding name}"></DataGridTextColumn>
            <DataGridTextColumn Header="Active" Binding="{Binding active}"></DataGridTextColumn>
            <DataGridTextColumn Header="Status" Binding="{Binding status}"></DataGridTextColumn>
        </DataGrid.Columns>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectionChanged">
                <i:InvokeCommandAction Command="{Binding clientSelectionChanged_command}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </DataGrid>
    <V:Client HorizontalAlignment="Left" Margin="163,58,-140,0" VerticalAlignment="Top" Content="{Binding currentClient}" Height="97" Width="201"/>
</Grid>

Client.xaml

<UserControl x:Class="B2BNet.View.Client"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:VM="clr-namespace:B2BNet.ViewModel"
         xmlns:local="clr-namespace:B2BNet"
         mc:Ignorable="d">
<Grid>
    <Grid.DataContext>
        <VM:Client/>
    </Grid.DataContext>
    <Label x:Name="name" Content="Name:" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
    <Label x:Name="active" Content="Active:" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="9,41,0,0"/>
    <Label x:Name="status" Content="Status:" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10,72,0,0"/>
    <Label x:Name="namevalue" HorizontalAlignment="Left" Margin="59,10,0,0" VerticalAlignment="Top" Width="200" Height="26" Content="{Binding name}"/>
    <Label x:Name="activevalue" HorizontalAlignment="Left" Margin="59,41,0,0" VerticalAlignment="Top" Width="200" Height="26" Content="{Binding active}"/>
    <Label x:Name="statusvalue" HorizontalAlignment="Left" Margin="60,67,-1,0" VerticalAlignment="Top" Width="200" Height="26" Content="{Binding status}"/>

</Grid>

ViewModel - ClientPanel.cs

using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
using B2BNet.MVVM;

namespace B2BNet.ViewModel
{
    public class ClientPanel : ObservableObject
    {
        public string Title { get; set; }

        private Client _currentClient;
        public Client currentClient
        {
            get
            {
                return _currentClient;
            }
            set
            {
                _currentClient = value;
                RaisePropertyChangedEvent( "currentClient" );
                Utility.print("currentClient Changed: " + _currentClient.name);
                Mediator.Instance.Notify(ViewModelMessages.UpdateClientViews, currentClient);
            }
        }

        private ObservableCollection<Client> _Clients;
        public ObservableCollection<Client> Clients
        {
            get
            {
                return _Clients;
            }
            set
            {
                _Clients = value;
                RaisePropertyChangedEvent("Clients");
                Utility.print("Clients Changed");
            }
        }


        public ClientPanel()
        {
            Title = "Clients";
            Clients = new ObservableCollection<Client>();

            for ( int i = 0; i < 10; i++ )
            {
                Clients.Add( new Client { name = "Client-" + i, status = "Current", active = true } );
            }
            currentClient = Clients.First();
            currentClient.setUpdateCallback();
        }

        ////////////
        //COMMANDS//
        ////////////
        public ICommand clientSelectionChanged_command
        {
            get { return new DelegateCommand( clientSelectionChanged ); }
        }
        private void clientSelectionChanged( object parameter )
        {
            B2BNet.Utility.print("clientSelectionChanged");
        }
    }
}

ViewModel - Client.cs

using System;
using B2BNet.MVVM;

namespace B2BNet.ViewModel
{
    public class Client : ObservableObject
    {

        private String _name;
        public String name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
                RaisePropertyChangedEvent( "name" );
                Utility.print("Client.name changed: " + value );
            }
        }

        private Boolean _active;
        public Boolean active
        {
            get
            {
                return _active;
            }
            set
            {
                _active = value;
                RaisePropertyChangedEvent( "active" );
                Utility.print("Client.active changed" + value );
            }
        }

        private String _status;
        public String status
        {
            get
            {
                return _status;
            }
            set
            {
                _status = value;
                RaisePropertyChangedEvent( "status" );
                Utility.print("Client.status changed" + value );
            }
        }

        public Client()
        {
            name = "Set in Client Constuctor";
            status = "Set in Client Constructor";
        }

        public void setUpdateCallback()
        {
            ////////////
            //Mediator//
            ////////////
            //Register a callback with the Mediator for Client
            Mediator.Instance.Register(
                (Object o) =>
                {
                    Client client = (Client)o;
                    name = client.name;
                    active = client.active;
                    status = client.status;
                }, B2BNet.MVVM.ViewModelMessages.UpdateClientViews
            );
        }
   }
}

Very Basic MVVM Framework

ObservableObject.cs

using System.ComponentModel;

namespace B2BNet.MVVM
{
    public class ObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void RaisePropertyChangedEvent( string propertyName )
        {
            var handler = PropertyChanged;
            if ( handler != null )
                handler( this, new PropertyChangedEventArgs( propertyName ) );
        }
    }
}

Mediator.cs

using System;
using B2BNet.lib;

namespace B2BNet.MVVM
{
    /// <summary>
    /// Available cross ViewModel messages
    /// </summary>
    public enum ViewModelMessages { UpdateClientViews = 1,
                                    DebugUpdated = 2
                                   };


    public sealed class Mediator
    {
        #region Data
        static readonly Mediator instance = new Mediator();
        private volatile object locker = new object();

        MultiDictionary<ViewModelMessages, Action<Object>> internalList
            = new MultiDictionary<ViewModelMessages, Action<Object>>();
        #endregion

        #region Ctor
        //CTORs
        static Mediator()
        {


        }

        private Mediator()
        {

        }
        #endregion

        #region Public Properties

        /// <summary>
        /// The singleton instance
        /// </summary>
        public static Mediator Instance
        {
            get
            {
                return instance;
            }
        }

        #endregion

        #region Public Methods
        /// <summary>
        /// Registers a callback to a specific message
        /// </summary>
        /// <param name="callback">The callback to use 
        /// when the message it seen</param>
        /// <param name="message">The message to 
        /// register to</param>
        public void Register(Action<Object> callback, 
            ViewModelMessages message)
        {
            internalList.AddValue(message, callback);
        }


        /// <summary>
        /// Notify all callbacks that are registed to the specific message
        /// </summary>
        /// <param name="message">The message for the notify by</param>
        /// <param name="args">The arguments for the message</param>
        public void Notify(ViewModelMessages message, 
            object args)
        {
            if (internalList.ContainsKey(message))
            {
                //forward the message to all listeners
                foreach (Action<object> callback in 
                    internalList[message])
                        callback(args);
            }
        }
        #endregion    
    }
}

DelegateCommand.cs

using System;
using System.Windows.Input;

namespace B2BNet.MVVM
{
    public class DelegateCommand : ICommand
    {
        private readonly Action<object> _action;

        public DelegateCommand( Action<object> action )
        {
            _action = action;
        }

        public void Execute( object parameter )
        {
            _action( parameter );
        }

        public bool CanExecute( object parameter )
        {
            return true;
        }

        #pragma warning disable 67
        public event EventHandler CanExecuteChanged;
        #pragma warning restore 67
    }
}

THE SOLUTION

The solution was a simple one. Thanks for the quick response Rachel :). I simple removed the following from each of the Views:

<!--<Grid.DataContext>
        <VM:ClientPanel/>
    </Grid.DataContext>-->

<!--<Grid.DataContext>
        <VM:Client/>
    </Grid.DataContext>-->

and it works perfectly, even allowing me to update the datagrid:

enter image description here

2
Your view models are privateMartin
I just updated the classes to public but the behavior is the same.Corbin Tarrant
The first thing I noticed is that your UserControls hardcode a DataContext, which means no DataContext can be passed into it. That's probably your issue.Rachel

2 Answers

3
votes

The problem is you are hardcoding the DataContext in your UserControls.

So your Client UserControl has it's DataContext hardcoded to a new instance of Client, and it is NOT the same instance that your ClientPanel UserControl is using. So the <V:Client Content="{Binding currentClient}" binding is pointing to a different instance of the property than the one the DataGrid is using.

So get rid of

<Grid.DataContext>
    <VM:Client/>
</Grid.DataContext>

and change your Content binding to a DataContext one

<V:Client DataContext="{Binding currentClient}" .../>

and it should work fine.

I thought for sure I had some rant on SO about why you should NEVER hardcode the DataContext property of a UserControl, but the closest thing I can find right now is this. Hopefully it can help you out :)

0
votes

Oh boy that's a lot of code and I'm not sure I understand everything you're trying to do. Here are a few important notes when binding with WPF.

  1. Make sure the properties have public { get; set; }
  2. If the value is going to be changing, make sure you add Mode=TwoWay, UpdateSourceTrigger=PropertyChanged to your bindings. This keeps your view and view model in sync and tells each other to update.
  3. Use ObservableCollections and BindingLists for collections. Lists don't seem to work very well.
  4. Make sure the DataContext matches the properties you are trying to bind to. If your DataContext doesn't have those properties, they aren't going to update.