ViewModel with null properties

Jun 20, 2011 at 3:30 PM

Hi, I had a pretty basic MVVM setup working with Simple MVVM toolkit, and recently I've encountered an issue where the ViewModel has all null properties and I'm not entirely sure why.

So, as an example, I have the following structure:

Settings.xaml -> main view with data grids, etc.

Settings.xaml.cs -> code behind (contains very little)

SettingsViewModel.cs -> view model class with properties for the xaml view

ISettingsServiceAgent.cs -> interface for service agent

SettingsServiceAgent.cs -> implementation of interface

----

So, in my Settings.xaml, I have a basic data grid, which I want to bind to an observable collection on the view model.  Here is the relevant snippet(s)

<navigation:Page x:Class="ServiceInspection.Views.Settings" 
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
           xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
           xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
           xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
           xmlns:view="clr-namespace:ServiceInspection.Views"
           xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
           xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit"
           xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
           mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1000"
           Title="Settings Page"
           Style="{StaticResource PageStyle}"
           Loaded="Page_Loaded"
           DataContext="{Binding Source={StaticResource Locator}, Path=SettingsViewModel}" >
    <Grid x:Name="LayoutRoot">
        <ScrollViewer VerticalScrollBarVisibility="Auto">

            <StackPanel Orientation="Vertical">
                <!-- _______________________________ TITLE _________________________________________ -->
                <TextBlock Margin="15,15,15,0" Text="Add, Edit, Remove Service Inspection Items" Style="{StaticResource HeaderTextStyle}" />

                <!-- _______________________________ INSPECTION GROUPS _____________________________ -->
                <Border BorderBrush="Silver" BorderThickness="1" Height="142" Name="border1" Width="Auto" Margin="15,0,15,5">
                    <toolkit:DockPanel Margin="5" FlowDirection="LeftToRight">
                        <!-- _______________________ GROUP BUTTONS __________________________ -->
                        <StackPanel Orientation="Vertical" toolkit:DockPanel.Dock="Left">
                            <TextBlock Height="23" Name="groupLabel" Text="Inspection Groups" Margin="5,5,5,0" />
                            <Button Content="Add Group" Height="23" Name="addGroupButton" Width="95" Margin="5,0,5,5" Click="addGroupButton_Click" />
                            <Button Content="Remove Group" Height="23" Name="delGroupButton" Width="95" Margin="5" Click="delGroupButton_Click" />
                            <Button Content="Clone Group" Height="23" Name="button1" Width="95" Margin="5" Click="cloneGroupButton_Click" />
                        </StackPanel>
                        <!-- _______________________ GROUP GRID ____________________________ -->
                        <sdk:DataGrid Name="groupGrid" AutoGenerateColumns="False" Margin="5" toolkit:DockPanel.Dock="Right" Height="Auto" Width="Auto"
                                      ItemsSource="{Binding Groups}" 
                                      SelectedItem="{Binding SelectedGroup, Mode=TwoWay}"
                                      RowEditEnded="groups_RowEditEnded">
                            <sdk:DataGrid.Columns>
                                <sdk:DataGridTextColumn Binding="{Binding Path=GroupName}" Width="*" Header="Name" />
                                
                            </sdk:DataGrid.Columns>
                        </sdk:DataGrid>
                       
                    </toolkit:DockPanel>
                </Border>

When the user has logged in, I load the Groups from my service agent.  After the entity has been loaded, I set the Groups property -- and note that I don't allow null -- if the value is null, I create an empty collection in the setter (from SettingsViewModel.cs).  This code all seems to work just fine, and if I step it in the debugger, I can see the entity was loaded, and the Groups property is set and it contains objects in the collection.  The problem is that nothing shows up in the view.  The grid doesn't show any data, even though I know the data is there.  See more below...

        public SettingsViewModel() 
        {
            if (IsInDesignMode == false)
            {
                WebContext.Current.Authentication.LoggedIn += (s, e) => OnLoggedIn(s, e);
                WebContext.Current.Authentication.LoggedOut += (s, e) => OnLoggedOut(s, e);
            }
        }

        // ctor that accepts IXxxServiceAgent
        public SettingsViewModel(ISettingsServiceAgent serviceAgent) : this()
        {
            this.serviceAgent = serviceAgent;
        }

private void OnLoggedIn(object sender, AuthenticationEventArgs e)
        {
            LoadDealer();
        }

       public void LoadDealer()
        {
            IsBusy = true;
            serviceAgent.GetDealer(DealerLoaded);
        }

       private void DealerLoaded(Dealer dealer, Exception error)
        {
            Debug.WriteLine("Dealer {0} was loaded", dealer.DealerName);
            if (error == null)
            {
                Groups = new ObservableCollection<Group>(dealer.Groups);
            }
            else
            {
                NotifyError("Unable to retrieve group descriptions", error);
            }
            if (Groups.Count > 0) { SelectedGroup = Groups[0]; }
            IsBusy = false;
        }

private ObservableCollection<Group> groups;
        public ObservableCollection<Group> Groups
        {
            get { return groups; }
            set
            {
                if (value == null) { groups = new ObservableCollection<Group>(); }
                else { groups = value; }
                NotifyPropertyChanged(m => m.Groups);
            }
        }

 

So, I have a couple buttons in the UI above the grid...one to add a Group, one to remove a Group, and one to Clone a group.  The buttons are tied to a click handler in the code behind which calls a method in the view model.  For example, here is the path when the add button is clicked:

 

        private void addGroupButton_Click(object sender, RoutedEventArgs e)
        {
            model.AddGroup(new Group());
        }


And, here's the method in the view model:

        public void AddGroup(Group group)
        {
            if (Groups != null)
            {
                serviceAgent.AddGroup(group);
                Groups.Add(group);
                SelectGroup(group);
            }
        }

The problem here is that when I breakpoint this and follow it down, the Groups property on the SettingsViewModel class is always null -- which makes no sense to me -- since I followed the path that originally set this property and it did set it to a non-null value of a collection that contains entities.  This leads me to believe that something is going wrong with the ServiceLocator pattern -- like it is overwriting the view model class, perhaps with a new object -- somehow I lose the original object?  

The bizarre thing is that this was working fine until recently and I've poured over the code several times looking for what change has caused it to stop working, and I cannot pinpoint any specific thing -- which is why I'm posting it here to try to get a second look.  If anyone has any insight, I'd love to hear about it.  

 

Thanks in advance,

Davis

 

Jun 20, 2011 at 3:45 PM

I think I see what is happening now after adding some debug trace statements in various places, it seems like the order of things follows:

a) user logs in and WebContext.Current.LoggedIn event is fired

b) event handler in SettingsViewModel runs for LoggedIn event and entity is loaded in background from service agent

c) groups is set on view model property ok

d) then i navigate to that view, and the service locator injects a new view model into the view, and the other one is not connected to the view.  I determine this by watching the order of debug output statements -- I see the Debug.WriteLine statement (below) issued last after the entity was loaded....

so, the service locator / injector does a sort of lazy injection, and even though my code is running in the background with a viewmodel / service agent pairing, these two aren't connected in any way to the actual xaml view -- they are constructed and executed beforehand b/c I have tied the WebContext.Current.Authentication.LoggedIn event to the view-model's constructor.

the bizarre thing is how it works at all then, since the view-model has to get the service agent from the locator class.

is there a better way to set this up?

The code in the injector / locator looks like this:

[ImportMany]
        public Lazy<ISettingsServiceAgent, IServiceAgentMetadata>[] SettingsServiceAgents { get; set; }
        public SettingsViewModel SettingsViewModel
        {
            get
            {
                var serviceAgent = SettingsServiceAgents
                    .Where(sa => sa.Metadata.AgentType == agentType).FirstOrDefault();
                SettingsViewModel viewModel = null;
                if (serviceAgent != null) viewModel = new SettingsViewModel(serviceAgent.Value);
                else if (DesignerProperties.IsInDesignTool) viewModel = new SettingsViewModel();

                Debug.WriteLine("returning new settings view model from locator");
                return viewModel;
            }
        }

Jul 1, 2011 at 3:02 PM

@Davis: It looks like this post slipped under my radar and I didn't notice it until recently - sorry about that.  Were you able to figure out what is happening here?

Let me outline the normal sequence of events with an injected view-model locator.  The way it works is that views set their DataContext directly to a property on the ViewModelLocator class, which creates the ViewModel on the fly, possibly injecting a service agent into the ctor of the ViewModel.  This means that the ViewModel is not created until the View needs it, and it can be garage collected along with the View when the View is no longer needed.

The part of this that can be confusing is how the service agent is created.  Simple MVVM Toolkit uses MEF (Managed Extensibility Framework), which is built into .NET 4 and Silverlight 4, to create the service agent lazily. The reason MEF is used for this is to perform dependency injection based on whether you want to use a real or mock service agent, which is specified using the service agent type property in the locator.  MEF creates either a real or a mock service agent when serviceAgent.Value is called. At any rate, the service agent should be created when the ViewModel ctor is called.  However, the service agent will not be created if MEF does not find a service agent with its type set to Real or Mock.

One other thing you might want to watch out for is that services are always called asynchronously in Silverlight. So while the service agent will be created in the ctor of the ViewModel, calling the service agent to supply data to the ViewModel will take place asynchronously in the background.

I hope this helps!

Cheers,

Tony