OnNavigatedTo is not invoked when navigating to a view in module - c#

I'm new to Prism and trying to make a simple modular app with it (see the full code).
I'm actually trying to pass a parameter to a view, so in the ModuleAViewViewModel of the ModuleAView I'm implementing INavigationAware, this way:
public class ModuleAViewViewModel : BindableBase, INavigationAware
{
private int passedId;
public int PassedId
{
get => passedId;
set => SetProperty(ref passedId, value);
}
// In the following 3 methods, a breakpoint never gets hit.
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
PassedId = (int)navigationContext.Parameters["Id"];
}
}
This is the corresponding ModuleAView.
<UserControl x:Class="ModularSample1.Modules.ModuleA.Views.ModuleAView"
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:ModularSample1.Modules.ModuleA.Views"
xmlns:viewmodels="clr-namespace:ModularSample1.Modules.ModuleA.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<viewmodels:ModuleAViewViewModel x:Key="moduleAViewModel"/>
</UserControl.Resources>
<Grid DataContext="{StaticResource moduleAViewModel}">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock
Grid.Row="1"
Text="{Binding PassedId, StringFormat='Passed Id: {0}'}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="34"/>
</Grid>
</UserControl>
And the ViewModel that actually navigates to that view looks like this:
public class MainWindowViewModel : BindableBase
{
private readonly IRegionManager regionManager;
public MainWindowViewModel(IRegionManager regionManager, IModuleManager moduleManager)
{
this.regionManager = regionManager;
moduleManager.Run();
Modules = moduleManager.Modules.ToList();
}
private List<IModuleInfo> modules;
private IModuleInfo selectedModule;
public List<IModuleInfo> Modules
{
get => modules;
set => SetProperty(ref modules, value);
}
public IModuleInfo SelectedModule
{
get => selectedModule;
set
{
if (SetProperty(ref selectedModule, value))
{
var Id = new Random().Next(1, 12);
var navigationParameters = new NavigationParameters
{
{ "Id", Id }
};
regionManager.RequestNavigate(
"SelectedModuleRegion",
$"{selectedModule.ModuleName}View",
navigationParameters);
}
}
}
}
What I'm missing here?

You define the view model for your ModuleAView in its Resources and set it on a child Grid via reference. Prism looks for the DataContext on the view itself to call a view model that implements INavigationAware, not a child control, so it does not find your view model. From the documentation:
During navigation, Prism checks to see whether the view implements the INavigationAware interface; if it does, it calls the required methods during navigation. Prism also checks to see whether the object set as the view's DataContext implements this interface; if it does, it calls the required methods during navigation.
In order to make it work, set the data context directly on the ModuleAView and remove it from Grid.
<UserControl x:Class="ModularSample1.Modules.ModuleA.Views.ModuleAView"
...>
<UserControl.DataContext>
<viewmodels:ModuleAViewViewModel/>
</UserControl.DataContext>
<Grid>
<!-- ...grid definitions. -->
</Grid>
</UserControl>
A better alternative is to remove setting the data context manually completely from your ModuleAView and register your ModuleAView directly with ModuleAViewViewModel. Prism's navigation service will automatically resolve the view model during navigation and assign it as data context.
containerRegistry.RegisterForNavigation<ModuleAView, ModuleAViewViewModel>();
You can even simplify this, by following the naming conventions for views and view models regarding the ViewModelLocator, which will be used in navigation to resolve view models for views.
ViewModels are in the same assembly as the view types
ViewModels are in a .ViewModels child namespace
Views are in a .Views child namespace
ViewModel names correspond with view names and end with "ViewModel.
You violate the last rule, so renaming ModuleAViewViewModel to ModuleAViewModel and just removing the manual setting of the data context will enable Prism to automatically find the corresponding view model for the view that you registered with RegisterForNavigation.
containerRegistry.RegisterForNavigation<ModuleAView>();

Related

Unable to register View to ViewModel manually using Prism.Mvvm.ViewModelLocationProvider

Folder structure -
Project Named 'Home'
|> Views > HomeView.xaml
|> ViewModels > HomeViewModel.cs
|> HomeModule.cs (an IModule class)
HomeView.XAML (Note: AutoWireViewModel is set to FALSE)
<UserControl
x:Class="Tally.Sync.Home.Views.HomeView"
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:local="clr-namespace:Tally.Sync.Home.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
d:DesignHeight="450"
d:DesignWidth="800"
prism:ViewModelLocator.AutoWireViewModel="False"
mc:Ignorable="d">
<Grid>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="28"
Text="{Binding Message}" />
</Grid>
</UserControl>
HomeViewModel.cs
public class HomeViewModel : BindableBase
{
private string _message = "Hi There!";
public string Message
{
get { return _message; }
set { SetProperty(ref _message, value); }
}
public HomeViewModel()
{
}
}
HomeModule.cs
public class HomeModule : IModule
{
private readonly IRegionManager _regionManager;
public HomeModule(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public void OnInitialized(IContainerProvider containerProvider)
{
_regionManager.RegisterViewWithRegion("ContentRegion", typeof(HomeView));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
ViewModelLocationProvider.Register<HomeView, HomeViewModel>();
}
}
As you can see I am registering the HomeView & HomeViewModel with the ViewModelLocationProvider. But this doesn't work. My code is running as expected when the AutoWireViewModel is set to True
What am I doing wrong? I know I can auto wire it, but I am trying to learn Prism and one of videos by Brian Lagunas mentions wiring manually is faster as reflection won't be required.
When you register views and view models manually to the view model locator, you still have to enable auto-wiring in the view model, it will just change how the ViewModelLocator resolves the view and its view model. Setting False will disable it, so it does not resolve anything.
prism:ViewModelLocator.AutoWireViewModel="True"
As you can see from the reference source for ViewModelLocationProvider, registered views will be checked first when resolving the view model. If there are no registrations, the fallback to resolving with the naming convention via reflection will be used.
public static void AutoWireViewModelChanged(object view, Action<object, object> setDataContextCallback)
{
object viewModel = GetViewModelForView(view);
if (viewModel == null)
{
// Try to resolve the view model via regsitrations
var viewModelType = GetViewModelTypeForView(view.GetType());
// If views and view models are not registerd, fallback to reflection
if (viewModelType == null)
viewModelType = _defaultViewTypeToViewModelTypeResolver(view.GetType());
//...
}
// ...
}
As the ViewModelLocator will resolve the view both via registration or the naming convention fallback, you did not notice a difference. If you rename your HomeViewModel to HomeFooBar or anything else that does not fit the naming convention, the fallback will fail and you will see that it works as described above.
[...] wiring manually is faster as reflection won't be required.
I think that there are many misunderstandings concerning reflection. In this case with a few views you will not even notice any difference and it is much more convenient. Do not optimize prematurely.

INavigationAware can't navigate away

I'm using Prism Unity, I have an abstract RecordViewModel : BindableBase, RecordListViewModel : RecordViewModel, and RecordUpdateViewModel: RecordViewModel, INavigationAware. There is also a separate Navigation Module and my MainWindow has 2 regions, NavigationRegion and ContentRegion. All RecordViews reside in ContentRegion. For whatever reason, whether I make a GoBack button or click on a button in the NavigationRegion, I cannot leave the Update view. I have narrowed down that the problem is in the ViewModel and that I'm missing something for INavigationAware. Please tell me what I'm missing or did wrong, thank you.
public class RecordUpdateViewModel : RecordViewModel, INavigationAware
{
private IRegionNavigationJournal navigationJournal;
public RecordUpdateViewModel(IRecordService context) : base(context)
{
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
throw new NotImplementedException();
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
//irrelevant to problem logic to bring in Record.Id
navigationJournal = navigationContext.NavigationService.Journal;
}
}
Edit
Just incase I screwed up elsewhere here's my registrations in the module.cs
container.RegisterType<IRecordService, RecordService>();
container.RegisterTypeForNavigation<RecordListView>();
container.RegisterTypeForNavigation<RecordUpdateView>();
regionManager.RegisterViewWithRegion("ContentRegion", typeof(RecordListView));
regionManager.RegisterViewWithRegion("ContentRegion", typeof(RecordUpdateView));
I navigate to the views with regionManager.RequestNavigate("ContentRegion", "RecordUpdateView", parameter) and if I don't use INavigationAware on the UpdateView, all buttons work, but when I put it back on I can't navigate away.
Edit2
Here is the XAML for the ListView that navigates to the UpdateView and the bound command
<UserControl x:Class="App.Record.Views.RecordListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:prism="http://prismlibrary.com/"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
prism:ViewModelLocator.AutoWireViewModel="True">
<DockPanel>
<WrapPanel>
<Button Content="Edit"
Command="{Binding EditCommand}"/>
</WrapPanel>
<DataGrid>
<--Irrelevant-->
</DataGrid>
</DockPanel>
</UserControl>
Command
private DelegateCommand editCommand;
public DelegateCommand EditCommand => editCommand ?? (editCommand = new DelegateCommand(EditRecord));
private const string RecordID = "RecordID";
void EditCommand()
{
var parameter = new NavigationParameters();
parameter.Add("RecordID", SelectedRecord.ID);
regionManager.RequestNavigate("ContentRegion", "RecordUpdateView", parameter);
}
Commands for the Navigation menu buttons work the same, and any view not using INavigationAware can be navigated away from.
INavigationAware has nothing to do with navigating from view to view. It simply give you an easy way to pass parameters between the view. I think what you're looking for is something called IRegionManager then you can do somehting like regionManager.NavigateTo(regionName,viewName) you will have to register the view with your container. Something like container.RegisterType<object,Views.View>(ViewNames.ViewName); and of course ViewNames.ViewName is a constant string with the name of the view.
I shall hang my head in shame... look to the OnNavigatedFrom... and all 3 INavigationAware Members for anyone else that looks to this... and remove the NotImplementedExceptions that are auto generated. If you don't need that void, it has to be there so INavigationAware doesn't yell at you for not fully implementing it, but there doesn't have to be any logic in those voids if you don't need them.

WPF, MVVM and PRISM - No Parameterless Constructor defined for this object

Answer
OK so adding the suggested code given by E-Bat didn't have any affect until I started a new project and copied all the code across verbatim. I can only assume there must be some background code within the ViewModelLocator on http://prismlibrary.com/ which did not update to take the parameterless constructor into account. Hope this helps anyone else with the same issue
Original Question
I have set up a MVVM project using prism. I have a MainWindow.xaml and 5 Views; ButtonsView, HeaderView, ProcessInputView, ProcessLogView and ProcessSelectionView which I am using, each View has an associated ViewModel.
MainWindow.xaml
<Window x:Class="TransactionAutomationTool.Views.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:local="clr-namespace:TransactionAutomationTool"
xmlns:views="clr-namespace:TransactionAutomationTool.Views"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800">
<Grid>
<views:HeaderView x:Name="HeaderViewControl" Margin="20,21,0,0" />
<views:ProcessSelectionView x:Name="ProcessSelectionViewControl" Margin="20,119,0,0" />
<views:ProcessInputView x:Name="ProcessInputViewControl" Margin="20,280,0,0" />
<views:ProcessLogView x:Name="ProcessLogView" Margin="298,105,0,0" />
<views:ButtonsView x:Name="ButtonViewControl" Margin="0,513,0,0" />
</Grid>
MainWindowViewModel
public class MainWindowViewModel: BindableBase
{
public IEventAggregator _events;
private UserPrincipal userPrincipal;
public MainWindowViewModel(IEventAggregator events)
{
_events = events;
userPrincipal = UserPrincipal.Current;
_events.GetEvent<HeaderLoaded>().Subscribe(HeaderHasBeenLoaded);
}
private void HeaderHasBeenLoaded()
{
_events.GetEvent<UserNameUpdate>().Publish(string.Format("{0} {1}", userPrincipal.GivenName, userPrincipal.Surname));
}
}
When I try to view the MainWindow in design mode I get the following Error
Screenshot of MainWindow In design Mode
No Parameterless constructor found for this object - This Highlights the HeaderView and the ButtonsView
Both the HeaderViewModel and ButtonsViewModel take IEventAggregator as a parameter within their constructor where as the rest of the ViewModels do not. I am assuming this is where the errors are coming from.
HeaderViewModel
public class HeaderViewModel: BindableBase
{
private string userName;
private string runTime;
public string UserName
{
get { return userName; }
set { SetProperty(ref userName, value); }
}
public string RunTime
{
get { return runTime; }
set { SetProperty(ref runTime, value); }
}
public HeaderViewModel(IEventAggregator events)
{
events.GetEvent<RunTimeUpdate>().Subscribe(RunTimeUpdated);
events.GetEvent<UserNameUpdate>().Subscribe(UserNameUpdated);
events.GetEvent<HeaderLoaded>().Publish();
}
private void RunTimeUpdated(string newRunTime)
{
RunTime = newRunTime;
}
private void UserNameUpdated(string userName)
{
UserName = userName;
}
}
So how can I get round this error if I need to subscribe to these events and hence need the IEventAggregator passed into my ViewModels?
Do I need to register this within the Bootstrap via an override of the ConfigureContainer method? If so I'm not entirely sure how to do this.
Bootstrap
class Bootstraper: UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void InitializeShell()
{
Application.Current.MainWindow.Show();
}
}
The application builds successfully and runs successfully but it is just when trying to view the MainWindow in the designer that I get this message.
Any help would be greatly appreciated.
EDIT
All my view constructors just have the initalizeComponent methods and take no parameters
The answer marked as accepted addresses the exception, but doesn't answer the question about why. Also, that approach will make unit testing quite difficult as you will be setting the datacontext to a specific object instead of passing in a dependency.
The reason you are getting the exception is because the HeaderView is not being instantiated by the container(by default it's UnityContainer).
You are constructing the entirety of MainWindow at design time instead of individual pieces. Try the following in MainWindow
<Grid>
<Grid.RowDefinitions>
<RowDefinitions />
<RowDefinitions />
<RowDefinitions />
<RowDefinitions />
<RowDefinitions />
</Grid.RowDefinitions>
<ContentControl Grid.Row="0" prism.RegionManager.RegionName="Row0Region" />
<ContentControl Grid.Row="1" prism.RegionManager.RegionName="Row1Region" />
<ContentControl Grid.Row="2" prism.RegionManager.RegionName="Row2Region" />
<ContentControl Grid.Row="3" prism.RegionManager.RegionName="Row3Region" />
<ContentControl Grid.Row="4" prism.RegionManager.RegionName="Row4Region" />
</Grid>
Then you can either use View Discovery or View Injection. For View Discovery, you can do something like
this.RegionManager.RegisterViewWithRegion("Row0Region", HeaderView()) and so on for rest of the views.
You can register views with regions in initialize method of modules or somewhere else. Upto you as to where you do that. You could override the Run method of the bootstrapper. Once the base run method has finished, you can register your views.
When the main window is displayed, all the regions will be discovered and RegionManager will populate the regions with the views that have been registered with each of the regions.
The regionmanager will instantiate the views using the container. When the container constructs each of the views, their viewmodels will be auto wired up. Also the IEventAggregator will be provided to the HeaderView's viewmodel.
This article is based on prism 4 - https://www.codeproject.com/Articles/165376/A-Prism-Application-Checklist but it talks about how to construct views.
Your view is trying to execute logic that only make sense at runtime, so you need to make sure that you are not in design mode:
public HeaderView()
{
if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
{
var svc = ServiceLocator.Current;
var eventAggregator = svc.GetInstance<IEventAggregator>();
this.DataContext = new HeaderViewModel(eventAggregator);
}
InitializeComponent();
}
EDIT:
For support of designtime view model have a look here
Basically you need to provide a parameterless constructor for you ViewModel to support design mode.

WPF Mvvm navigation with parameters

Following this tutorial (among others) and reading questions asked here I've constructed a navigation mechanism that will allow me to pass parameters between my ViewModels:
Object base - every view model inherits from it:
public abstract class ObjectBase : INotifyPropertyChanged
{
//INotifyPropertyChanged members
...
//Navigation handling
public abstract ObjectBase BackLocation { get; }
public abstract event Action<ObjectBase> NavigateTo;
public abstract string ViewHeader { get; }
}
MainViewModel - in charge of navigation:
public class MainViewModel : ObjectBase
{
private ObjectBase _selectedView;
private CommandBase _backCommand;
public MainViewModel()
{
SelectedView = new FirstViewModel();
}
public ObjectBase SelectedView
{
get { return _selectedView; }
set
{
SetProperty(ref _selectedView, value);
//register to the navigation event of the new view
SelectedView.NavigateTo += (target)=> { SelectedView = target; };
}
}
//This command is bound to a Back button on the main view
public CommandBase BackCommand
{
get { return _backCommand ?? (_backCommand = new CommandBase(Back)); }
}
private void Back(object obj)
{
if (SelectedView.BackLocation != null)
{
SelectedView = SelectedView.BackLocation;
}
else
{
Application.Current.Shutdown();
}
}
}
And the main view:
<Window ...
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type vm:FirstViewModel}">
<views:FirstView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SecondViewModel}">
<views:SecondView/>
</DataTemplate>
</Window.Resources>
<ContentPresenter Content="{Binding SelectedView}"/>
</Window>
My problem is: If I set the DataTemplates in the main view like the above it makes each view aware of it's DataContext so if I want to add the DataContext explicitly to a view in order to use intellisense like this:
<UserControl x:Class="Wpf_NavigationTest.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:Wpf_NavigationTest.ViewModels">
<!--this causes the view model's constructor to get called again-->
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
<Grid>
<TextBlock Text="User control 1" FontSize="40"/>
</Grid>
the View Model's constructor is called twice, losing the parameters passed by the Navigate event.
The problem here is that you are setting the DataContext inside your UserControl, and also in your main view model.
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
The code above is instantiating a new FirstViewModel every time this UserControl is created. Therefore when the control gets created by the ContentControl (based on the DataTemplate), it then goes ahead and also creates a new FirstViewModel.
So, the solution here is to remove the UserControl.DataContext declaration in the UserControl, and you can instead set the DataContext of the ContentControl to that of your SelectedView.
<ContentPresenter Content="{Binding SelectedView}"
DataContext="{Binding SelectedView}"/>
In order to use multiple view models to a single view, you can simply add another DataTemplate:
<DataTemplate DataType="{x:Type vm:ThirdViewModel}">
<views:SecondView/>
</DataTemplate>
For Design-Time data (to get the intellisense), you can make use of d:DataContext as explained in this article.
This will require you to set up some view models as static resources, I would recommend creating them in a separate ResourceDictionary.

Bind SelectedValue of UserControl to ViewModel

In my solution; I have two projects: One is a WPF UserControl Library, and the other is a WPF Application.
The usercontrol is pretty straightforward; it's a label and a combo box that will show the installed printers.
In the WPF application; I want to use this usercontrol. The selected value will be stored in user settings.
The problem I'm having is that I can't seem to get the proper binding to work. What I need to happen is to be able to set the SelectedValue of the UserControl when the MainWindow loads; as well as access the SelectedValue of the UserControl when I go to save my settings.
My code is below, could someone point me in the right direction?
PrintQueue user control:
<UserControl x:Class="WpfControls.PrintQueue"
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:wpfControls="clr-namespace:WpfControls"
mc:Ignorable="d">
<UserControl.DataContext>
<wpfControls:PrintQueueViewModel/>
</UserControl.DataContext>
<Grid>
<StackPanel Orientation="Horizontal">
<Label Content="Selected Printer:"></Label>
<ComboBox ItemsSource="{Binding Path=PrintQueues, Mode=OneWay}" DisplayMemberPath="Name" SelectedValuePath="Name" Width="200" SelectedValue="{Binding Path=SelectedPrinterName, Mode=TwoWay}"></ComboBox>
</StackPanel>
</Grid>
</UserControl>
Print Queue Codebehind:
public partial class PrintQueue : UserControl
{
public static readonly DependencyProperty CurrentPrinterNameProperty =
DependencyProperty.Register("CurrentPrinterName", typeof (string), typeof (PrintQueue), new PropertyMetadata(default(string)));
public string CurrentPrinterName
{
get { return (DataContext as PrintQueueViewModel).SelectedPrinterName; }
set { (DataContext as PrintQueueViewModel).SelectedPrinterName = value; }
}
public PrintQueue()
{
InitializeComponent();
DataContext = new PrintQueueViewModel();
}
}
PrintQueue View Model:
public class PrintQueueViewModel : ViewModelBase
{
private ObservableCollection<System.Printing.PrintQueue> printQueues;
public ObservableCollection<System.Printing.PrintQueue> PrintQueues
{
get { return printQueues; }
set
{
printQueues = value;
NotifyPropertyChanged(() => PrintQueues);
}
}
private string selectedPrinterName;
public string SelectedPrinterName
{
get { return selectedPrinterName; }
set
{
selectedPrinterName = value;
NotifyPropertyChanged(() => SelectedPrinterName);
}
}
public PrintQueueViewModel()
{
PrintQueues = GetPrintQueues();
}
private static ObservableCollection<System.Printing.PrintQueue> GetPrintQueues()
{
var ps = new PrintServer();
return new ObservableCollection<System.Printing.PrintQueue>(ps.GetPrintQueues(new[]
{
EnumeratedPrintQueueTypes.Local,
EnumeratedPrintQueueTypes.Connections
}));
}
}
Main Window:
<Window x:Class="WPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfControls="clr-namespace:WpfControls;assembly=WpfControls" xmlns:wpfApp="clr-namespace:WPFApp"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<wpfApp:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<wpfControls:PrintQueue CurrentPrinterName="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.PrinterName, Mode=TwoWay}"></wpfControls:PrintQueue>
</StackPanel>
</Grid>
</Window>
Main Window View Model:
public class MainWindowViewModel : ViewModelBase
{
private string printerName;
public string PrinterName
{
get { return printerName; }
set
{
printerName = value;
NotifyPropertyChanged(() => PrinterName);
}
}
public MainWindowViewModel()
{
PrinterName = "Lexmark T656 PS3";
}
}
Controls in a library need to expose DependencyProperties that you can bind to in your view. Just like WPF's TextBox exposes a Text property.
Your PrintQueue control doesn't expose anything, and instead keeps all its state in a viewmodel that nothing outside can access. Your MainWindowViewModel has no way of getting at the stuff inside PrintQueueViewModel.
You need to expose SelectedPrinterName as a DependencyProperty in the code behind of your PrintQueue xaml. Then in MainWindow.xaml you can bind it to MainWindowViewModel.PrinterName.
If you want to user ViewModels all the way through instead, then MainWindowViewModel should be creating PrintQueueViewModel itself so it can access the properties within.
As per your update / comment:
Unfortunately DependencyProperties don't work like that. The getters/setters aren't even used most of the time, and they should ONLY update the property itself. You're sort of halfway between two worlds at the moment.
If I were in your position, and assuming you can change the library so PrintQueue.xaml doesn't have a hardcoded VM instance in the view, I would just create the PrintQueueViewModel yourself. That's how MVVM is supposed to work:
ViewModel:
public class MainWindowViewModel : ViewModelBase
{
public PrintQueueViewModel PrintQueue { get; private set; }
public MainWindowViewModel()
{
PrintQueue = new PrintQueueViewModel();
PrintQueue.SelectedPrinterName = "Lexmark T656 PS3";
}
}
View:
<Window x:Class="WPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfControls="clr-namespace:WpfControls;assembly=WpfControls" xmlns:wpfApp="clr-namespace:WPFApp"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<wpfApp:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<wpfControls:PrintQueue DataContext="{Binding PrintQueue}"/>
</StackPanel>
</Grid>
</Window>
Again though, control libraries generally don't have view models, and expose their state via dependency properties since they're designed to be used in XAML.
Component libraries may expose view models, but in that case they wouldn't hard code the view model in the view.
Did you write the library? If not, how did the author expect people to use it?
I think with this small changes everything should work
<ComboBox ItemsSource="{Binding Path=PrintQueues, Mode=OneWay}" DisplayMemberPath="Name" Width="200" SelectedItem="{Binding Path=SelectedPrinter, Mode=TwoWay}"></ComboBox>
private System.Printing.PrintQueue selectedPrinter;
public System.Printing.PrintQueue SelectedPrinter
{
get { return selectedPrinter; }
set
{
selectedPrinter = value;
NotifyPropertyChanged(() => SelectedPrinter);
}
}
Now from the main window you can modify SelectedPrinter on the viewmodel and the change should be reflected on the view
(PrintQueue.DataContext as PrintQueueViewModel).SelectedPrinter = ...
I tried your code and your bindings of the PrintQueueView to the corresponding view model work fine. Your problem is that the MainWindowViewModel does not know about the PrintQueueViewModel and thus cannot retrieve the value of the selected printer when the main window closes (I guess that is the scenario you want to implement).
The quickest solution to your problem would be to do the following steps:
In MainWindow.xaml, give PrintQueue a Name so you can access it in the code behind
In MainWindow.xaml.cs, override the OnClosing method. In it you can retrieve the view model as follows: var viewModel = (PrintQueueViewModel)PrintQueue.DataContext;. After that you can retrieve the selected value and save it or whatever.
In the MainWindow constructor after InitializeComponent, you can retrieve your saved value from a file and set it on the PrintQueueViewModel by retrieving it the same way as in the previous step.
Whole code in MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Retrieve your selected printer here; in this case, I just set it directly
var selectedPrinter = "Lexmark T656 PS3";
var viewModel = (PrintQueueViewModel)PrintQueue.DataContext;
viewModel.SelectedPrinterName = selectedPrinter;
}
protected override void OnClosing(CancelEventArgs e)
{
var viewModel = (PrintQueueViewModel)PrintQueue.DataContext;
var selectedPrinterName = viewModel.SelectedPrinterName;
// Save the name of the selected printer here
base.OnClosing(e);
}
}
Please remember that the major point of view models is the ability to unit-test GUI logic and to disconnect GUI appearance and logic. Your view models should not be able to retrieve all the possible printers of your system but should obtain these values by e.g. Dependency Injection. I would advise you to read about SOLID programming.

Categories

Resources