locked
Using multiple ViewModel's in WinRT

    Question

  • OS: Windows 8.1
    IDE: VS 2013 Express for Windows
    Project: Universal
    Target: Windows 8.1, Phone 8.1
    IoC: MEF

    My Windows App is being based on this Guideline "Hierarchical navigation, start to finish" on the Windows Dev Center. I'm using MEF to inject my ViewModels into Views.

    The MainPage is in the .Shared Project and the HubPage, ItemPage and SectionPage are in there perspective Window and Phone Projects.

    Each Page has its own ViewModel located in an assembly called UILogic. During startup all ViewModels are successfully injected into there Views. However looking at the Immediate Window I get this Binding Errors:

    Windows 8.1

    Error: BindingExpression path error: 'HubSectionHeaderCommand' property not found on 'NathsarTS.UILogic.ViewModels.MainPageViewModel, NathsarTS.UILogic, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. BindingExpression: Path='HubSectionHeaderCommand' DataItem='NathsarTS.UILogic.ViewModels.MainPageViewModel, NathsarTS.UILogic, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'; target element is 'Microsoft.Xaml.Interactions.Core.InvokeCommandAction' (Name='null'); target property is 'Command' (type 'ICommand')
    Error: BindingExpression path error: 'DefaultDataModel' property not found on 'NathsarTS.UILogic.ViewModels.MainPageViewModel, NathsarTS.UILogic, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. BindingExpression: Path='DefaultDataModel' DataItem='NathsarTS.UILogic.ViewModels.MainPageViewModel, NathsarTS.UILogic, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'; target element is 'Windows.UI.Xaml.Data.CollectionViewSource' (Name='null'); target property is 'Source' (type 'Object')
    Error: BindingExpression path error: 'NavigationHelper' property not found on 'NathsarTS.Views.HubPage'. BindingExpression: Path='NavigationHelper.GoBackCommand' DataItem='NathsarTS.Views.HubPage'; target element is 'Windows.UI.Xaml.Controls.Button' (Name='backButton'); target property is 'Command' (type 'ICommand')
    Error: BindingExpression path error: 'Section3Items' property not found on 'NathsarTS.UILogic.ViewModels.MainPageViewModel, NathsarTS.UILogic, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. BindingExpression: Path='Section3Items' DataItem='NathsarTS.UILogic.ViewModels.MainPageViewModel, NathsarTS.UILogic, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'; target element is 'Windows.UI.Xaml.Controls.HubSection' (Name='null'); target property is 'DataContext' (type 'Object')

    The MainPage looks like this:

    <Grid x:Name="LayoutRoot" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Frame x:Name="rootFrame"/>

    The HubPage looks like this which is navigated too during startup:

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    	...
    	..
    	<Hub >
    		<interactivity:Interaction.Behaviors>
    			<core:EventTriggerBehavior EventName="SectionHeaderClick">
    				<core:InvokeCommandAction Command="{Binding HubSectionHeaderCommand}"/>
    			</core:EventTriggerBehavior>
    		</interactivity:Interaction.Behaviors>
    		<Hub.Header>
    			<!-- Back button and page title -->
    			<Grid>
    				...
    				..
    				<Button  x:Name="backButton" Style="{StaticResource NavigationBackButtonNormalStyle}"
    					Margin="0,0,39,0" 
    					VerticalAlignment="Top"
    					Command="{Binding NavigationHelper.GoBackCommand, ElementName=pageRoot}"
    					AutomationProperties.Name="Back"
    					AutomationProperties.AutomationId="BackButton"
    					AutomationProperties.ItemType="Navigation Button"/>
    				...
    				..
    			</Grid>
    		</Hub.Header>
    		...
    		..
    		<HubSection IsHeaderInteractive="True" 
    					DataContext="{Binding Section3Items}" 
    					d:DataContext="{Binding Groups[3], Source={d:DesignData Source=../NathsarTS.Shared/DataModel/SampleData.json, Type=data:SampleDataSource}}"
    					x:Uid="Section3Header" Header="Section 3" Padding="40,40,40,32">
    			<DataTemplate>
    				<GridView
    					x:Name="itemGridView"
    					ItemsSource="{Binding Items}"
    					Margin="-9,-14,0,0"
    					AutomationProperties.AutomationId="ItemGridView"
    					AutomationProperties.Name="Items In Group"
    					ItemTemplate="{StaticResource Standard310x260ItemTemplate}"
    					SelectionMode="None"
    					IsSwipeEnabled="false"
    					IsItemClickEnabled="True">
    					<interactivity:Interaction.Behaviors>
    						<core:EventTriggerBehavior EventName="ItemClick">
    							<core:InvokeCommandAction Command="{Binding HubSectionGridItemCommand}"/>
    						</core:EventTriggerBehavior>
    					</interactivity:Interaction.Behaviors>
    				</GridView>
    			</DataTemplate>
    		</HubSection>
    		...
    		..
    	</Hub>
    </Grid>


    My Bindings are being reported as not being found on the MainPageViewModel, but they are defined in the HubPageViewModel.

    I have MEF injecting the ViewModels this way:

    [Export(typeof(HubPage))]
    public sealed partial class HubPage : Page
    {
    	public HubPage()
    	{
    		this.InitializeComponent();
    		Debug.WriteLine("HubPage InitializeComponent");
    	}
    
    	[Import]
    	public NathsarTS.UILogic.Interfaces.IHubPageViewModel ViewModel
    	{
    		set
    		{
    			this.DataContext = value;
    		}
    		get
    		{
    			return DataContext as NathsarTS.UILogic.Interfaces.IHubPageViewModel;
    		}
    	}
    
    	/// <summary>
    	/// Called when a part's imports have been satisfied and it is safe to use.
    	/// </summary>
    	[OnImportsSatisfied]
    	public void OnImportsSatisfied()
    	{
    		// IPartImportsSatisfiedNotification is useful when you want to coordinate doing some work
    		// with imported parts independent of when the UI is visible.
    		Debug.WriteLine("HubPage OnImportsSatisfied instantiation");
    
    		//NathsarTS.UILogic.Interfaces.IObservableService ObservableService = ServiceLocator.Current.GetInstance<NathsarTS.UILogic.Interfaces.IObservableService>();
    		this.ViewModel.NavigationHelper = new NathsarTS.Common.Logic.NavigationHelper(this);
    		this.ViewModel.NavigationHelper.LoadState += this.ViewModel.NavigationHelperLoadState;
    	}
    
            /// <summary>
            /// Invoked when this page is about to be displayed in a Frame.
            /// </summary>
            /// <param name="e">Event data that describes how this page was reached.
            /// This parameter is typically used to configure the page.</param>
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                // TODO: Prepare page for display here.
                Debug.WriteLine("HubPage OnNavigatedTo instantiation");

                // TODO: If your application contains multiple pages, ensure that you are
                // handling the hardware Back button by registering for the
                // Windows.Phone.UI.Input.HardwareButtons.BackPressed event.
                // If you are using the NavigationHelper provided by some templates,
                // this event is handled for you.
            }
    }

    ViewModels per PageView isn't permitted in WinRT?

    IN ADDTION:
    I've also notice this during startup, the HubPage is being Initialize twice.  Why?:

    MainPage InitializeComponent
    PropertiesService instantiation
    MainPageViewModel instantiation
    MainPage OnImportsSatisfied instantiation
    HubPage InitializeComponent
    HubPageViewModel instantiation
    HubPage OnImportsSatisfied instantiation
    HubPage InitializeComponent
    HubPage OnNavigatedTo instantiation

    This is what I'm doing during startup:

    /// <summary>
    /// Invoked when the application is launched normally by the end user.  Other entry points
    /// will be used when the application is launched to open a specific file, to display
    /// search results, and so forth.
    /// </summary>
    /// <param name="e">Details about the launch request and process.</param>
    protected override async void OnLaunched(LaunchActivatedEventArgs args)
    {
    	this._configuration.WithAssembly(typeof(App).GetTypeInfo().Assembly)
    		.WithAssembly(typeof(NathsarTS.UILogic.UILogicBusinessLogic).GetTypeInfo().Assembly)
    		.WithAssembly(typeof(NathsarTS.Common.CommonBusinessLogic).GetTypeInfo().Assembly)
    		.WithAssembly(typeof(NathsarTS.ODSDocuments.ODSDocumentsBusinessLogic).GetTypeInfo().Assembly);
    	this._compositionHost = this._configuration.CreateContainer();
    
    	await ShowWindow(args);
    }
    
    private async Task ShowWindow(LaunchActivatedEventArgs e)
    {
    	MainPage mainPage = Window.Current.Content as MainPage;
    	Frame rootFrame = null;
    
    	// Do not repeat app initialization when the Window already has content,
    	// just ensure that the window is active
    	if (mainPage == null)
    	{
    		mainPage = _compositionHost.GetExport<MainPage>();
    	}
    
    	// Retrieve the root Frame to act as the navigation context and navigate to the first page
    	// Don't change the name of "rootFrame" in MainPage.xaml unless you change it here to match.
    	rootFrame = (Frame)mainPage.FindName("rootFrame");
    	if (rootFrame != null)
    	{
    		// Associate the frame with a SuspensionManager key.
    		SuspensionManager.RegisterFrame(rootFrame, "AppFrame");
    
    		// TODO: change this value to a cache size that is appropriate for your application
    		rootFrame.CacheSize = 1;
    
    		if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
    		{
    			// Restore the saved session state only when appropriate
    			try
    			{
    				await SuspensionManager.RestoreAsync();
    			}
    			catch (SuspensionManagerException)
    			{
    				//Something went wrong restoring state.
    				//Assume there is no state and continue
    			}
    		}
    
    		// Place the main page in the current Window.
    		Window.Current.Content = mainPage;
    
    		if (rootFrame.Content == null)
    		{
    			// When the navigation stack isn't restored navigate to the first page,
    			// configuring the new page by passing required information as a navigation
    			// parameter
    			HubPage hubPage = _compositionHost.GetExport<HubPage>();
    			if (!rootFrame.Navigate(hubPage.GetType(), e.Arguments))
    			{
    				throw new Exception("Failed to create initial page");
    			}
    		}
    
    	}
    
    	// Ensure the current window is active
    	Window.Current.Activate();
    }
    

    Thanks!...


    Code is like a box of chocolates!...

    • Edited by VcDeveloper Tuesday, September 16, 2014 7:26 PM Additional Info
    Tuesday, September 16, 2014 6:39 PM

Answers

  • Hi Bret,

    Thanks for the reply!  I'm using MEF to inject the ViewModels (above code block 5, 6), but I was able to fix my duplicates and properly have the correct ViewModel connected to the PageView.  The problem was the .Navigation for 1) keep re-initializing the Page constructor after MEF does its initializing, 2) for some reason the HubPage DataContext gets lost.

    So, this is how I solved it:

    protected override async Task OnLaunchApplicationAsync(LaunchActivatedEventArgs args)
    {
    	Debug.WriteLine("OnLaunchApplicationAsync...");
    
    	MainPage mainPage = _compositionHost.GetExport<MainPage>();
    	Window.Current.Content = mainPage;
    	Frame rootFrame = (Frame)mainPage.FindName("rootFrame");
    
    	if (rootFrame != null)
    	{
    		if (rootFrame.Content == null)
    		{
    			// When the navigation stack isn't restored navigate to the first page,
    			// configuring the new page by passing required information as a navigation
    			// parameter
    			HubPage hubPage = _compositionHost.GetExport<HubPage>();
    			rootFrame.Content = hubPage;
    		}
    	}
    
    	Window.Current.Activate();
    	await Task.FromResult<object>(null);
    }

    Instead of using the .Navigation in the initial startup I just assigned the PageType to the .Content.


    Code is like a box of chocolates!...





    • Marked as answer by VcDeveloper Tuesday, September 16, 2014 10:04 PM
    • Edited by VcDeveloper Tuesday, September 16, 2014 10:09 PM
    Tuesday, September 16, 2014 10:04 PM

All replies

  • I didn't see the HubPage.ViewModel property set anywhere in what you pasted.   I see in the output that  a HubPageViewModel was initialized, but where is the HubPage.ViewModel = new HubPageViewModel() call?  Sharing a complete sample would speed things along as well.

    Bret Bentzinger (MSFT) @awehellyeah

    Tuesday, September 16, 2014 8:50 PM
    Moderator
  • Hi Bret,

    Thanks for the reply!  I'm using MEF to inject the ViewModels (above code block 5, 6), but I was able to fix my duplicates and properly have the correct ViewModel connected to the PageView.  The problem was the .Navigation for 1) keep re-initializing the Page constructor after MEF does its initializing, 2) for some reason the HubPage DataContext gets lost.

    So, this is how I solved it:

    protected override async Task OnLaunchApplicationAsync(LaunchActivatedEventArgs args)
    {
    	Debug.WriteLine("OnLaunchApplicationAsync...");
    
    	MainPage mainPage = _compositionHost.GetExport<MainPage>();
    	Window.Current.Content = mainPage;
    	Frame rootFrame = (Frame)mainPage.FindName("rootFrame");
    
    	if (rootFrame != null)
    	{
    		if (rootFrame.Content == null)
    		{
    			// When the navigation stack isn't restored navigate to the first page,
    			// configuring the new page by passing required information as a navigation
    			// parameter
    			HubPage hubPage = _compositionHost.GetExport<HubPage>();
    			rootFrame.Content = hubPage;
    		}
    	}
    
    	Window.Current.Activate();
    	await Task.FromResult<object>(null);
    }

    Instead of using the .Navigation in the initial startup I just assigned the PageType to the .Content.


    Code is like a box of chocolates!...





    • Marked as answer by VcDeveloper Tuesday, September 16, 2014 10:04 PM
    • Edited by VcDeveloper Tuesday, September 16, 2014 10:09 PM
    Tuesday, September 16, 2014 10:04 PM