I have a ContentControl in DayView.xaml whose content binds to the CurrentSongViewModel property in DayViewModel.cs . The CurrentSongViewModel is DataTemplated in the ContentControl to display its respective view depending on the view model (either DefaultSongViewModel or EditSongViewModel), and both of the DataTemplates are confirmed to work. When I click the 'Edit' button on DefaultSongView.xaml, the EditSongCommand executes and sets the CurrentSongViewModel to a new EditSongViewModel. The CurrentSongViewModel setter calls OnPropertyChanged(), but the ContentControl content is not updating! I have set break points on the OnPropertyChanged() call and it is calling it. I have no idea why its not updating...
DayView.xaml
<UserControl x:Class="Calandar.Views.DayView"
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:local="clr-namespace:Calandar.Views"
xmlns:viewmodels="clr-namespace:Calandar.ViewModels"
d:DataContext="{d:DesignInstance Type=viewmodels:DayViewModel}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Background="LightSteelBlue">
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="Day" Grid.Row="0" FontSize="35" FontFamily="Yu Gothic UI Semibold" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400"/>
<ColumnDefinition Width="400"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" >
<Border Style="{StaticResource PurpleBorder}">
<!-- The binding that isnt working -->
<ContentControl Content="{Binding CurrentSongViewModel}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:DefaultSongViewModel}">
<local:DefaultSongView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:EditSongViewModel}">
<local:EditSongView/>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</Border>
</StackPanel>
</Grid>
</Grid>
</UserControl>
DefaultSongView.xaml
<UserControl x:Class="Calandar.Views.DefaultSongView"
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:local="clr-namespace:Calandar.Views"
xmlns:viewmodels="clr-namespace:Calandar.ViewModels"
d:DataContext="{d:DesignInstance Type=viewmodels:DayViewModel}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.DataContext>
<viewmodels:DayViewModel/>
</Grid.DataContext>
<StackPanel>
<DockPanel >
<Button Content="Edit" Command="{Binding EditSongCommand}"
Style="{StaticResource CollectionModifierButton}" DockPanel.Dock="Right"/>
<TextBlock Text="Songs" Style="{StaticResource BoxTitleText}" DockPanel.Dock="Top"/>
</DockPanel>
<ListBox ItemsSource="{Binding SongList}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=.}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</UserControl>
DayViewModel.cs
public class DayViewModel : ViewModelBase
{
// content view models
private ViewModelBase _currentSongViewModel;
public ViewModelBase CurrentSongViewModel
{
get { return _currentSongViewModel; }
set
{
_currentSongViewModel = value;
OnPropertyChanged(nameof(CurrentSongViewModel));
}
}
// Song list
public ObservableCollection<string> SongList { get; set; }
// Commands
public ICommand EditSongCommand => new EditSongsCommand(this);
// Constructor
public DayViewModel()
{
_currentSongViewModel = new DefaultSongViewModel();
SongList = new ObservableCollection<string>();
foreach (string line in File.ReadLines(#"C:\Users\person\source\repos\Calandar\DataFiles\Songs.txt"))
{
SongList.Add(line);
}
}
}
EditSongCommand.cs
public class EditSongsCommand : CommandBase
{
DayViewModel _vm;
public override bool CanExecute(object parameter) => true;
public override void Execute(object parameter)
{
_vm.CurrentSongViewModel = new EditSongViewModel();
}
public EditSongsCommand(DayViewModel vm)
{
_vm = vm;
}
}
ViewModelBase.cs
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string PropertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
}
}
The following markup sets the DataContext to another instance of the DayViewModel:
<Grid.DataContext>
<viewmodels:DayViewModel/>
</Grid.DataContext>
You should remove it from DefaultSongView.xaml and bind to the command of the parent view model using a RelativeSource:
<Button
Content="Edit"
Command="{Binding DataContext.EditSongCommand, RelativeSource={RelativeSource AncestorType=ContentControl}}"
Related
I'm trying to create a sort of ShellView without using thirdparty frameworks, and I'm doing trying to do it via. ContentControls and UserControls.
I can navigate/switch the UserControls that is showed inside the ContentControls as long as the commands are fired from outside the UserControls, but nothing is happening when I move the code inside a button in a UserControl.
Currently I'm having one MainWindow.XAML, where I have one ContentControl.
This ContentControl is hosting either a LoginWindowUserControl, or a UserWindowUserControl - I want to be able switch the UserControl from within one of the UserControls.
My MainWindow.XAML looks like this:
<Window x:Class="ModelHealthApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vms="clr-namespace:ModelHealthApplication.ViewModels"
xmlns:ia="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:views="clr-namespace:ModelHealthApplication.Views.UserControls"
xmlns:local="clr-namespace:ModelHealthApplication"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<DataTemplate DataType="{x:Type vms:CurrentWindowUserStateViewModel}">
<views:UserView />
</DataTemplate>
<DataTemplate DataType="{x:Type vms:LoginWindowViewModel}">
<views:LoginView />
</DataTemplate>
<vms:NavigationViewModel x:Key="nVm" />
</Window.Resources>
<ia:Interaction.Triggers>
<ia:EventTrigger EventName="Loaded">
<ia:InvokeCommandAction Command="{Binding Source={StaticResource nVm}, Path=OpenLoginWindowCommand}" />
</ia:EventTrigger>
</ia:Interaction.Triggers>
<Grid DataContext="{StaticResource nVm}">
<DockPanel>
<Button Content="Test" DockPanel.Dock="Left" Command="{Binding OpenUserWindowStateCommand}" />
<ContentControl x:Name="WindowUserState" Content="{Binding CurrentWindowUserState}" />
</DockPanel>
</Grid>
And my UserWindowUserControl looks like this:
<UserControl x:Class="ModelHealthApplication.Views.UserControls.UserView"
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:vms="clr-namespace:ModelHealthApplication.ViewModels"
xmlns:views="clr-namespace:ModelHealthApplication.Views.UserControls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ModelHealthApplication.Views.UserControls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<DataTemplate DataType="{x:Type vms:MyModelsViewModel}">
<views:MyModelsView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vms:MyAccountViewModel}">
<views:MyAccountView />
</DataTemplate>
<vms:NavigationViewModel x:Key="nVm" />
</UserControl.Resources>
<Grid>
<DockPanel DataContext="{StaticResource nVm}">
<Grid DockPanel.Dock="Top" Background="{StaticResource MainBlue}" Height="25">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="0">
<Button Content="My Models"
HorizontalAlignment="Center"
Command="{Binding OpenMyModelsCommand}"
Style="{StaticResource NavButtonStyle}"/>
<Button Content="My Account"
HorizontalAlignment="Center"
Command="{Binding OpenMyAccountCommand}"
Style="{StaticResource NavButtonStyle}"
/>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Grid.Column="1" Margin="0, 0, 10, 0">
<TextBlock VerticalAlignment="Center" Foreground="White">
<Run Text="Logged in as:" FontWeight="Bold"/>
<Run Text="{Binding LoggedInAs}" d:Text="TestUser" />
</TextBlock>
<TextBlock Margin="20, 0 ,0 ,0" Text="Log Out" VerticalAlignment="Center" TextDecorations="Underline" Foreground="{StaticResource ComplenetarySecondOrange}" Background="{DynamicResource MainBlue}" Cursor="Hand">
<TextBlock.InputBindings>
<MouseBinding Command="{Binding OpenLoginWindowCommand}" MouseAction="LeftClick" />
</TextBlock.InputBindings>
</TextBlock>
</StackPanel>
</Grid>
<ContentControl x:Name="Pages" Content="{Binding SelectedViewModel}" />
</DockPanel>
</Grid>
</UserControl>
When I press the "Log Out" button/textblock I want to navigate back to the login window, but I can only do this from buttons that exist outside the UserControl.
I've read several other post similar to this, but I haven't found a solution that fits my needs - I tried using RelativeSource but without success - maybe someone can see what I'm doing wrong.
I'm using a "NavigationViewModel" to hold the commands which as I mentioned, works fine outside the UserControls:
using ModelHealthApplication.Commands;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace ModelHealthApplication.ViewModels
{
public class NavigationViewModel : INotifyPropertyChanged
{
public ICommand OpenMyModelsCommand { get; set; }
public ICommand OpenMyAccountCommand { get; set; }
public ICommand OpenUserWindowStateCommand { get; set; }
public ICommand OpenLoginWindowCommand { get; set; }
private object currentWindowUserState;
public object CurrentWindowUserState
{
get { return currentWindowUserState; }
set
{
currentWindowUserState = value;
OnPropertyChanged("CurrentWindowUserState");
}
}
private object selectedViewModel;
public object SelectedViewModel
{
get { return selectedViewModel; }
set
{
selectedViewModel = value;
OnPropertyChanged("SelectedViewModel");
}
}
public NavigationViewModel()
{
OpenMyModelsCommand = new OpenMyModelsCommand(this);
OpenMyAccountCommand = new OpenAccountCommand(this);
OpenUserWindowStateCommand = new OpenUserWindowStateCommand(this);
OpenLoginWindowCommand = new OpenLoginWindowCommand(this);
}
public void OpenUserWindowState(object obj)
{
CurrentWindowUserState = new CurrentWindowUserStateViewModel();
}
public void OpenLoginWindow(object obj)
{
CurrentWindowUserState = new LoginWindowViewModel();
}
public void OpenMyModels(object obj)
{
SelectedViewModel = new MyModelsViewModel();
}
public void OpenMyAccount(object obj)
{
SelectedViewModel = new MyAccountViewModel();
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
}
Ended up solving this with a great tutorial from SingletonSean on Youtube.
Posting here if anyone comes across this, and has the same issue.
Singleton Sean MVVM Navigatoin
I'm trying to figure out how to bind a property from my MainWindowViewModel to a ContentControl that is based on another View.
RelativeSource Binding seems not to work since the View is in another xaml file?
I tried with dependancy property but I didn't understand how to do this correctly I guess.
What I want to achieve here is that when I type in the TextBox of the MainWindow that all 3 views (contentcontrols) also view the updated data. It should be a demo to illustrate that in MVVM the ViewModel can change without knowledge of the Views and different Views react to it.
Sadly RelativeSource Binding and DependancyProperty didn't work for me or I missed a point.
MainWindow.xaml
<Window x:Class="MVVMDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<TextBlock Text="MVVM Demo" HorizontalAlignment="Center" FontSize="20" FontWeight="Bold"/>
<TextBox Text="{Binding TestString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="200" Background="Black" Foreground="White" Margin="0 20 0 0"/>
<Grid Margin="0 20 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="300"/>
</Grid.RowDefinitions>
<ContentControl Grid.Column="0" Grid.Row="0" Content="{Binding ViewOne}"/>
<ContentControl Grid.Column="1" Grid.Row="0" Content="{Binding ViewTwo}"/>
<ContentControl Grid.Column="2" Grid.Row="0" Content="{Binding ViewThree}"/>
</Grid>
</StackPanel>
</Window>
MainWindowViewModel
public class MainWindowViewModel : ViewModelBase
{
/// <summary>
/// String to change the reaction of views
/// </summary>
private string _TestString;
public string TestString
{
get { return _TestString; }
set { _TestString = value; NotifyPropertyChanged(); }
}
private object _ViewOne { get; set; }
public object ViewOne
{
get { return _ViewOne; }
set { _ViewOne = value; NotifyPropertyChanged(); }
}
private object _ViewTwo { get; set; }
public object ViewTwo
{
get { return _ViewTwo; }
set { _ViewTwo = value; NotifyPropertyChanged(); }
}
private object _ViewThree { get; set; }
public object ViewThree
{
get { return _ViewThree; }
set { _ViewThree = value; NotifyPropertyChanged(); }
}
public MainWindowViewModel()
{
ViewOne = new ViewOne();
ViewTwo = new ViewTwo();
ViewThree = new ViewThree();
TestString = "ABC";
}
}
ViewOneViewModel
<UserControl x:Class="MVVMDemo.Views.ViewOne"
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:local="clr-namespace:MVVMDemo.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Background="White">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=TestString, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" Foreground="Black"/>
<TextBlock Text="{Binding TestStringProperty}" Foreground="Black"/>
</StackPanel>
</Grid>
</UserControl>
ViewOneViewModel
public class ViewOneViewModel : ViewModelBase
{
// this doesn't help
public static readonly DependencyProperty TestStringProperty =
DependencyProperty.Register("TestString", typeof(string), typeof(MainWindowViewModel), new PropertyMetadata(null));
}
I believe your issue lies here:
<TextBlock Text="{Binding Path=TestString, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" Foreground="Black"/>
Binding Path=TestString should instead be Binding Path=DataContext.TestString as the RelativeSource is looking at the window now, not the window's model.
I use .NETFramework,Version=v4.6.1
I have a Window, MainWindow. This is the XAML:
<Window x:Class="VexLibrary.DesktopClient.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VexLibrary.DesktopClient.Views"
Title="MainWindow" Height="600" Width="800">
<Grid>
<StackPanel>
<Grid Style="{StaticResource TitleBar}">
<Border Style="{StaticResource TitleBarBorder}">
<DockPanel>
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
<TextBlock Style="{StaticResource TitleBarIcon}" Text="" />
<Label Style="{StaticResource TitleBarTitle}" Content="{Binding Path=CurrentPageTitle, UpdateSourceTrigger=PropertyChanged}" ></Label>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
<Label Style="{StaticResource TitleBarTime}">12:05 AM</Label>
<StackPanel Orientation="Horizontal">
<Label Style="{StaticResource TitleBarUsername}">Hassan</Label>
<Button>
<TextBlock Style="{StaticResource TitleBarIcon}" Text="" />
</Button>
</StackPanel>
</StackPanel>
</DockPanel>
</Border>
</Grid>
<Frame Width="700" Height="507" Source="Pages/Dashboard.xaml" />
</StackPanel>
</Grid>
</Window>
Note the:
<Label Style="{StaticResource TitleBarTitle}" Content="{Binding Path=CurrentPageTitle, UpdateSourceTrigger=PropertyChanged}" ></Label>
The DataContext is set as follows in the MainWindow.xaml.cs constructor:
this.DataContext = new MainViewModel();
In the <Frame>, a Page Dashboard.xamlis loaded.
The page Dashboard.xaml has the source:
<Page x:Class="VexLibrary.DesktopClient.Views.Pages.Dashboard"
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:local="clr-namespace:VexLibrary.DesktopClient.Views.Pages"
mc:Ignorable="d"
d:DesignHeight="460" d:DesignWidth="690"
Title="Page1">
<Grid Width="690" Height="460" HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- Members, Users, Books -->
<!-- Returns, Subscriptions, Statistics -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Button Style="{StaticResource MenuButton}" Grid.Column="0" Grid.Row="0"></Button>
<Button Style="{StaticResource MenuButton}" Grid.Column="0" Grid.Row="1"></Button>
<Button Style="{StaticResource MenuButton}" Grid.Column="1" Grid.Row="0"></Button>
<Button Style="{StaticResource MenuButton}" Grid.Column="1" Grid.Row="1"></Button>
<Button Style="{StaticResource MenuButton}" Grid.Column="2" Grid.Row="0"></Button>
<Button Style="{StaticResource MenuButton}" Grid.Column="2" Grid.Row="1" Command="{Binding ViewStatistics}"></Button>
</Grid>
</Page>
In the Dashboard.xaml.cs constructor, I have defined the DataContext like this: DataContext = new DashboardViewModel();
The DashboardViewModel.cs source code is like this (omitted namespaces)
namespace VexLibrary.DesktopClient.ViewModels
{
class DashboardViewModel : ViewModel
{
private MainViewModel parentViewModel;
public DashboardViewModel()
{
this.parentViewModel = new MainViewModel();
}
public ICommand ViewStatistics
{
get
{
return new ActionCommand(p => this.parentViewModel.LoadPage("Statistics"));
}
}
}
}
Now, in this code, notice the Button with the Command:
<Button Style="{StaticResource MenuButton}" Grid.Column="2" Grid.Row="1" Command="{Binding ViewStatistics}"></Button>
It successfully calls the Command and the parent LoadPage method is executed correctly. The parent viewmodel looks like this:
namespace VexLibrary.DesktopClient.ViewModels
{
public class MainViewModel : ViewModel
{
private string currentPageTitle;
public string CurrentPageTitle
{
get
{
return this.currentPageTitle;
}
set
{
currentPageTitle = value;
NotifyPropertyChanged();
}
}
public void LoadPage(string pageName)
{
this.CurrentPageTitle = pageName;
Console.WriteLine(CurrentPageTitle);
}
}
}
The CurrentPageTitle is successfully updated. However, it is not updated in the view.
The parent view model inherits ViewModel which basically has this code:
namespace VexLibrary.Windows
{
public abstract class ViewModel : ObservableObject, IDataErrorInfo
{
public string this[string columnName]
{
get
{
return OnValidate(columnName);
}
}
[Obsolete]
public string Error
{
get
{
throw new NotImplementedException();
}
}
protected virtual string OnValidate(string propertyName)
{
var context = new ValidationContext(this)
{
MemberName = propertyName
};
var results = new Collection<ValidationResult>();
bool isValid = Validator.TryValidateObject(this, context, results, true);
if (!isValid)
{
ValidationResult result = results.SingleOrDefault(p =>
p.MemberNames.Any(memberName =>
memberName == propertyName));
return result == null ? null : result.ErrorMessage;
}
return null;
}
}
}
ObservableObject.cs:
namespace VexLibrary.Windows
{
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// [CallerMemberName] automatically resolves the property name for us.
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChangedEventHandler handler = PropertyChanged;
Console.WriteLine(handler == null);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
After debugging, I found out, the NotifyPropertyChanged is invoked, but the handler is always null. How do I fix this? This is not updating the text in the MainWindow.xaml. I tested to see if the property value is changed, and yes, it is changed in the MainViewModel.cs
Also, I tested whether the label itself is visible or not. For that, I gave the variable a value and it correctly displays, but it is not updated.
The DashboardViewModel is instantiating a new instance of the MainViewModel rather than using the instance assigned to the DataContext of the MainWindow (and therefore the instance the view is bound to).
For your code to work you need to pass the correct instance of the MainViewModel to the DashboardViewModel as it is this instance that will have a handler for the property changed event.
EDIT: As per the comment below, you should instantiate your sub viewmodels as follows:
namespace VexLibrary.DesktopClient.ViewModels
{
public class MainViewModel : ViewModel
{
private ViewModel _currentViewModel;
public MainViewModel()
{
_currentViewModel = new DashboardViewModel(this);
}
public ViewModel CurrentViewModel
{
get { return _currentViewModel; }
private set
{
_currentViewModel = value;
OnPropertyChanged();
}
}
}
}
You can then amend your Xaml such that the frame gets it's data context from the CurrentViewModel property as follows:
<Window x:Class="VexLibrary.DesktopClient.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VexLibrary.DesktopClient.Views"
Title="MainWindow" Height="600" Width="800">
<Grid>
<StackPanel>
<Grid Style="{StaticResource TitleBar}">
<Border Style="{StaticResource TitleBarBorder}">
<DockPanel>
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
<TextBlock Style="{StaticResource TitleBarIcon}" Text="" />
<Label Style="{StaticResource TitleBarTitle}" Content="{Binding Path=CurrentPageTitle, UpdateSourceTrigger=PropertyChanged}" ></Label>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
<Label Style="{StaticResource TitleBarTime}">12:05 AM</Label>
<StackPanel Orientation="Horizontal">
<Label Style="{StaticResource TitleBarUsername}">Hassan</Label>
<Button>
<TextBlock Style="{StaticResource TitleBarIcon}" Text="" />
</Button>
</StackPanel>
</StackPanel>
</DockPanel>
</Border>
</Grid>
<Frame Width="700" Height="507" Source="Pages/Dashboard.xaml" DataContext="{Binding CurrentViewModel}"/>
</StackPanel>
</Grid>
</Window>
And will then need to use some form of view location / navigation to change the frame to display the correct view. Some MVVM frameworks (for example, CaliburnMicro) can do this for you.
Again, in order to make this code testable, the instantiation of sub-viewmodels should be delegated to a factory class which is injected into the MainViewModel.
Hope it helps.
I'm using MVVM in a windows phone 8 application. I would like to move from 1 view model to another inside my shell view model. I can't seem to get the ContentControl to bind to a template that is a usercontrol/phoneApplicationPage over the view model.
What am I missing?
I'm trying to avoid things like MVVM light. (I want my app to be as small a download as possible) And this should be possible to do.
P.S. I'm still pretty new to WPF/WP8
Here is a sample of what I have so far, Excuse the dumb functionality :)
/** The Shell view **/
<phone:PhoneApplicationPage
x:Class="PhoneAppWithDataContext.Navigation.ViewModelNavigation"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
mc:Ignorable="d"
shell:SystemTray.IsVisible="True"
xmlns:vm="clr-namespace:PhoneAppWithDataContext.Navigation">
<phone:PhoneApplicationPage.DataContext>
<vm:AppViewModel/>
</phone:PhoneApplicationPage.DataContext>
<phone:PhoneApplicationPage.Resources>
<DataTemplate x:Key="MonthViewModel">
<vm:MonthViewControl/>
</DataTemplate>
</phone:PhoneApplicationPage.Resources>
<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--TitlePanel contains the name of the application and page title-->
<StackPanel Grid.Row="0" Margin="12,17,0,28">
<TextBlock Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ContentControl Content="{Binding CurrentViewModel}"
ContentTemplate="{Binding ContentTemplate}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"/>
</Grid>
<Button Content="Change VM" Command="{Binding ChangeViewModelCommand}"/>
</Grid>
</phone:PhoneApplicationPage>
/** Shell/Application View Model **/
public class AppViewModel : ViewModelBase
{
private ViewModelBase _currentViewModel;
private List<ViewModelBase> _viewModels = new List<ViewModelBase>();
private byte _month = 1;
public ViewModelBase CurrentViewModel
{
get
{
return _currentViewModel;
}
set
{
if (_currentViewModel == value)
return;
_currentViewModel = value;
NotifyPropertyChanged("CurrentViewModel");
}
}
public DataTemplate SelectedTemplate
{
get
{
if (_currentViewModel == null)
return null;
return DataTemplateSelector.GetTemplate(_currentViewModel);
}
}
public List<ViewModelBase> ViewModels
{
get
{
return _viewModels;
}
}
public AppViewModel()
{
ViewModels.Add(new MonthViewModel(_month));
CurrentViewModel = ViewModels.FirstOrDefault();
}
private ICommand _changeViewModelCommand;
public ICommand ChangeViewModelCommand
{
get
{
return _changeViewModelCommand ?? (_changeViewModelCommand = new GenericCommand(() =>
{
_month++;
var newVM = new MonthViewModel(_month);
ViewModels.Add(newVM);
CurrentViewModel = newVM;
}, true));
}
}
private void ChangeViewModel(ViewModelBase viewModel)
{
if (!ViewModels.Contains(viewModel))
ViewModels.Add(viewModel);
CurrentViewModel = ViewModels.FirstOrDefault(vm => vm == viewModel);
}
}
/** DataTemplateSelector **/
public static class DataTemplateSelector
{
public static DataTemplate GetTemplate(ViewModelBase param)
{
Type t = param.GetType();
return App.Current.Resources[t.Name] as DataTemplate;
}
}
/** User Control **/
<UserControl x:Class="PhoneAppWithDataContext.Navigation.MonthViewControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
d:DesignHeight="480" d:DesignWidth="480"
xmlns:vm="clr-namespace:PhoneAppWithDataContext.Navigation">
<UserControl.DataContext>
<vm:MonthViewModel/>
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Id" Width="100" VerticalAlignment="Center" Grid.Row="0" Grid.Column="0" />
<TextBlock Text="{Binding Id}" Width="100" VerticalAlignment="Center" Grid.Row="0" Grid.Column="1" />
<TextBlock Text="Name" Width="100" VerticalAlignment="Center" Grid.Row="1" Grid.Column="0" />
<TextBlock Text="{Binding Name}" Width="100" VerticalAlignment="Center" Grid.Row="1" Grid.Column="1" />
</Grid>
</UserControl>
/** ViewModelBase **/
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
/** View model that the user control should bind to **/
public sealed class MonthViewModel : ViewModelBase
{
private byte _id;
private string _name;
public MonthViewModel()
{
}
public MonthViewModel(byte id)
{
_id = id;
_name = "Month " + id.ToString() + " of the year";
}
public override string ToString()
{
return _name;
}
public byte Id
{
get
{
return _id;
}
}
public string Name
{
get
{
return _name;
}
}
}
I believe the problem here is:
<UserControl.DataContext>
<vm:MonthViewModel/>
</UserControl.DataContext>
When your Content is changed from one MonthViewModel to the next, the DataContext of the returned DataTemplate is set to the object bound to Content. Well, once that DataContext is set, you should be good to go, but once the UserControl is loaded, it is resetting the DataContext to a new instace of an empty MonthViewModel (vm:MonthViewModel). Get rid of that explicit DataContext declaration--in other words delete the code that I posted above.
That way, when you first call CurrentViewModel and INPC is raised, it won't reset the DataContext. When you switch between CurrentViewModel's that are of MonthViewModel type, your UserControl won't call InitializeComponent again, instead the DataContext will change.
EDIT
In addition, if you still aren't seeing changes, then I would point to SelectedTemplate property. Instead of the null check in the property, just pass null to the GetTemplate. Inside of GetTemplate, check for null and return null there if it is null.
XAML
<Window x:Class="Html5Mapper.Mapper.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpt="http://schemas.xceed.com/wpf/xaml/toolkit"
Title="HTML mapper" Height="300" Width="600" >
<Window.DataContext>
<Binding Path="Main" Source="{StaticResource Locator}"/>
</Window.DataContext>
<ListBox Name="lbFiles" ItemsSource="{Binding Filescollection, Mode=OneWay}" Width="240" Height="220">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<Image Source="HTML.png" Height="40" Width="32" />
<TextBlock Grid.Column="1" Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
MainViewModel.cs
public ObservableCollection<Files> Filescollection { get; set; }
public MainViewModel()
{
this.Filescollection = new ObservableCollection<Files>();
SelectFilesAction();
}
private void SelectFilesAction()
{
this.Filescollection.Add(new Files { Name = "index.html", Path = "C:\\Files"});
//lbFiles.ItemsSource = docs;
}
Q: What else do I need to get the docs object into the Listbox ?
In my opinion you are binding your controls to wrong datacontect, check output window for erros. You might want to set datacontext of main window to your MainViewModel (in codebehind or similar). Also why do you create another instance for docs? It is useless.
public ObservableCollection<Files> Filescollection {get;set;}
public MainViewModel()
{
this.Filescollection = new ObservableCollection<Files>();
SelectFilesAction();
}
private void SelectFilesAction()
{
Filescollection.Add(new Files { Name = "index.html", Path = "C:\\Files"});
}
Vidas is correct in that the DataContext of your Window is wrong, it needs to be your MainViewModel class.
Get rid of this:
<Window.DataContext>
<Binding Path="Main" Source="{StaticResource Locator}"/>
</Window.DataContext>
And add this to the Window tag:
<Window DataContext="{StaticResource MainViewModel}">
And that should do it.
<UserControl x:Class="RetailPOS.View.Usercontrols.MainWindow.Products"
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:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
xmlns:ctrl="clr-namespace:RetailPOS.View.Usercontrols.MainWindow"
d:DesignHeight="200" d:DesignWidth="490" **DataContext="{Binding Source={StaticResource Locator}, Path=CategoryVM}"**
xmlns:my="clr-namespace:WpfLab.Controls;assembly=WpfLab.Controls">
<UserControl.Resources>
</UserControl.Resources>
<Grid Width="490" Height="360">
<ListBox Name="LstProduct" ItemsSource="{Binding LstProduct}" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" Margin="0" Height="Auto" Width="490" >
</WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Button Margin="1" Content="{Binding Name}" Height="53" Background="{Binding Color}" HorizontalAlignment="Right" Width="79"
Command="{Binding DataContext.ShowProductCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}"
CommandParameter="{Binding}">
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
In the code behind
private ObservableCollection<ProductDTO> _lstProduct;
public ObservableCollection<ProductDTO> LstProduct
{
get { return _lstProduct; }
set
{
_lstProduct = value;
RaisePropertyChanged("LstProduct");
}
}
/// <summary>
/// Get all Commonly Used Products
/// </summary>
/// <returns>returns list of all Commonly Used products present in database</returns>
private void FillCommonProducts()
{
LstProduct = new ObservableCollection<ProductDTO>(from item in ServiceFactory.ServiceClient.GetCommonProduct()
select item);
}