I'm trying to get to grips with the concept of a ViewModelLocator (in MVVM Light, although the question applies to the concept of a ViewModelLocator in general, regardles of which MVVM framework is used), and I have a hard time figuring out how to use it.
As I understand it, your views use one of the properties on the singleton instance of the locator as their datacontext. The locator defines these various properties, and returns the proper viewmodel instance for each.
That's all fine, but I have trouble understanding how you actually populate these viewmodels with the model data the views are supposed to present.
For instance, suppose I have a view that presents a list of employees. I can create an EmployeesView and an EmployeesViewModel. In the ViewModelLocator, I can create a property that returns this EmployeesViewModel:
public EmployeesViewModel Employees
{
get
{
return ServiceLocator.Current.GetInstance<EmployeesViewModel>();
}
}
Now, the viewmodel needs a list of employees, so I can create some sort of dataservice that returns all employees, and register that with the Servicelocator in the ViewModelLocator's constructor:
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<IDataService, AllEmployeesDataService>();
}
So, this'll work, if I instantiate the EmployeesView the EmployeesViewModel will be instantiated and injected with a dataservice that returns all employees.
However, now I want to see the details of a certain employee that I just clicked in the EmployeesView. This employee presumably has some sort of Id by which to retrieve him/her from a database, or whatever.
I can create an EmployeeDetailsView and an EmployeeDetailsViewModel, and add a property to the ViewModelLocator:
public EmployeeDetailsViewModel EmployeeDetails
{
return ServiceLocator.Current.GetInstance<EmployeeDetailsViewModel>();
}
and perhaps register some sort of dataservice in the ViewModelLocator's constructor:
SimpleIoc.Default.Register<IDataService, EmployeeDetailsDataService>();
But how do I tell either the dataservice or the viewmodel which employee they're supposed to present the details for? Where do I pass the employee id?
Am I looking at this all wrong? Anyone know of any good examples? All the examples I can find just return the same single instance of each viewmodel.
A simple example I use is to use a dataservice that gets injected into the constructor of the viewmodel, like you have. That data service returns an observable collection of my objects (in your case employees.) I create a listbox and Detail grid in the same view. So I bind the listbox to the observable collection that I can style and sort by using a collectionviewsource. For the details, I create a grid with the required fields that I want to display. I create a property in my viewmodel for the selected item (SelectedEmployee) of the listbox and bind them together. THen I bind the detail grid to the SelectedEmployee. THis will cause the fields to show the values from the selected employee.
Now you can us this for all CRUD operations and you can bind the slecteditemchanged event of the listbox to a relay command and add your business logic as needed. Another thing to note is you can break this apart to support async operations. I have another implementation where I take the selected item changed event of the listbox and execute a async get function to get the selcted item.
I hope this helps
I find J King answer quite good, but I will like to shed some more light and options never the less.
In the comment you asked about what happens if it's not the same view?
Here's an implementation I have:
<ListBox x:Name="YourListView"
ItemsSource="{Binding SomeCollection}"
SelectedItem="{Binding SelectedItemObject,
UpdateSourceTrigger=PropertyChanged}"
ToolTip="Double click to edit"
>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header ="Edit me" Command="{Binding Edit_Command}"
CommandParameter="{Binding SelectedItemObject}"
/>
<MenuItem Header ="Delete me" Command="{Binding Delete_Command}"
CommandParameter="{Binding SelectedItemObject}"
/>
</ContextMenu>
</ListBox.ContextMenu>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<Command:EventToCommand Command="{Binding Edit_Command}"
CommandParameter="{Binding ElementName=YourListView,
Path=SelectedItem}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
Notice that either double clicking on an object in that list, or right clicking and selecting one of the two options (namely edit and delete), will Call a command with a paramater.
Now, when you use the ContextMenu since you right clicked and object, it's selected, and you can just send it.
In the case of the double click, I'm using the name of the listbox to get the item.
From here what I'll do in the viewmodel is something like:
private void Execute_Edit(object param)
{
var your_object = (cast_to_your_type)param;
Messenger.Default.Send(new SwitchView(new SomeViewModel(_dataService,your_object)));
}
The ICommand will call Execute_Edit, which in turn will send a message using the Messenger.
This is how I defined SwitchView:
/// <summary>
/// Used as message, to switch the view to a different one.
/// </summary>
public class SwitchView
{
public SwitchView(MyViewModelBase viewmodel)
{
ViewModel = viewmodel;
}
public MyViewModelBase ViewModel { get; set; }
}
My MainWindow is registered to listen to those messages,and we know what to change to (obviously, to the view model given : SomeViewModel). The SwitchView on the main class will change the ViewModel property to the property the message passed.
Here's what I have in my main view:
<Border >
<ContentControl Content="{Binding Current_VM}" />
</Border>
So, whatever the Current_VM property is set to, will show you that view.
Hope that helped :)
Related
I want to call controls inside view like button and item template inside viewmodel. Please tell how can I do that. My view contains following
<ItemsControl Name="cDetails"
Width="395"
ItemTemplate="{DynamicResource Test}"
ItemsSource="{Binding ViewModels}"
Visibility="{Binding IsLoaded,
Converter={StaticResource visibilityConverter}}">
<Button Name="btnComplete"
Grid.Column="1"
HorizontalAlignment="Center"
Command="{Binding AuditCommand}"
CommandParameter="1">
Complete
</Button>
Please tell how can I call these items in my viewmodel using vb.net.
Thanks
Accessing your view components from inside your viewmodel is not the way to do things in MVVM. Because it is specifically not designed to work this way, you will have to go out of your way to make it work. You should probably investigate how to accomplish your goals using MVVM properly, or forego using MVVM at all and do the work in your code-behind.
Since you have not described what your goal is, it is hard to provide specific recommendations. In general when using MVVM, you manipulate things in your viewmodel and set properties. Your view binds to these properties so that it updates appropriately as they are being set. Your viewmodel does not directly manipulate the views themselves, only the viewmodel properties that they are bound to.
For example, let's say you are updating the text on a TextBlock. You could do something like this in xaml:
<TextBlock Text="{Binding SomeText}" />
Then, your viewmodel (which should implement the INotifyPropertyChanged interface) defines this property and sets it as desired.
public string SomeText
{
get { return _someText; }
set
{
if (_someText != value)
{
_someText = value;
NotifyPropertyChanged("SomeText");
}
}
}
private string _someText;
...
// At any time, you can set the property, and the
// binding will update the text in the control for you.
SomeText = "Some text";
If you absolutely need to manipulate your views from code (or if you are not using MVVM), the appropriate place for that sort of code is the "xaml.cs" file next to your view (the code-behind). You can assign a name to anything in your xaml using syntax like <TextBlock x:Name="SomeTextBlock" /> and then access it from the code-behind as a member variable with the same name. For example, you could do SomeTextBlock.Text = "Some text". However, this is not usually necessary for the vast majority of use cases if you are using MVVM.
You shouldn't try to access controls directly from the ViewModel. The ViewModel must not know about the View implementation.
Instead, in WPF we connect View and ViewModel through Bindings. Bindings connect Properties of controls in the View, with Properties in the ViewModel.
Commands are a special type of Property that can be bound as actions for controls like Button.
In your example, you would need to have these properties in your ViewModel:
A collection named ViewModels
A boolean named IsLoaded
And an ICommand named AuditCommand
By managing those properties, you should be able to control what's shown in your View and its behavior.
If you need more control, create more Bindings to other properties, or create some events in your ViewModel and manage them from your View's code-behind.
I am new to WPF and the MVVM pattern. I am trying to build a 'step by step' or 'wizard' like application.
So the user should first login then select some data and finally the selected data should be stored somewhere. (This flow never changes!)
However I decided to use DataTemplates and different ViewModels for each Template and a MainViewModel for the Window which populates the Templates. (Think this should be ok regarding to different Posts here)
But now my problems are starting. I know how I can change the current view from the MainViewModel BUT I want to change the current View from the inner ViewModel. And I want to be able to collect data from one inner ViewModel to another and I have no clue how this can work.
MainViewModel:
public class MainWindowViewModel : ViewModelBase
{
public ViewModelBase CurrentView {get; set;}
public MainWindowViewModel()
{
CurrentView = new ViewModelA;
}
}
XAML:
<Window x:Class="PUSEImporter.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:VM="clr-namespace:PUSEImporter.ViewModel"
xmlns:V="clr-namespace:PUSEImporter.View">
<Window.DataContext>
<VM:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type VM:ViewModelA}">
<V:Detail />
</DataTemplate>
<DataTemplate DataType="{x:Type VM:ViewModelB}">
<V:Overview />
</DataTemplate>
</Window.Resources>
<ContentPresenter Content="{Binding CurrentView}"/>
</Window>
So think about a button in ViewModelA (or the View from ViewModelA) and now I want to switch to ViewModelB when someone clicks on the button. And not enough the data which is collected by ViewModelA should also be available in ViewModelB.
Is this possible and the preferred way of using this techniques or do I misunderstand some concepts?
(And if this is true how should i handle things like that?)
Thanks in advance!
There are many ways to achieve what you want. In MVVM, there's one view model to each view... therefore, if your main view has a child view, then your main view model should have a property of the type of another view model. In this instance, you can add a delegate to the child view model and register a handler for it in the main view model.
This will enable you to effectively pass a signal to the main view model from the child view model, which you can react to in the parent view model in any way you want to. Rather than explain the whole story again here, please see my answers from the Passing parameters between viewmodels and How to call functions in a main view model from other view models? posts here on Stack Overflow for more information on how to achieve this.
In my MVVM Light application I do a search in a customer list. The search narrows the customer list which are displayed in a master/detail view with a datagrid (the master CustomerSearchResultView) and a separately defined usercontrol with FirstName, Lastname, Address etc, etc (the detail - CustomerSearchDetailView). Here are the main content of the master/detail view:
<StackPanel MinWidth="150" >
<TextBlock Text="Customer Search Result List" />
<Grid>
<DataGrid Name="CustomerList" ItemsSource="{Binding SearchResult}" SelectedItem="{Binding SelectedRow, Mode=TwoWay}" >
.....
</DataGrid>
</Grid>
<Grid Grid.Column="2">
<TextBlock Text="Customer Details" Style="{StaticResource Heading2}" Margin="30,-23,0,0"/>
<content:CustomerSearchDetail DataContext="{Binding SelectedRow}" />
</Grid>
</Grid>
</StackPanel>
Both have their corresponding ViewModels. Please remark the DC for the CustomerSearchDetail, SelectedRow - it is a property on the CustomerSearchResultViewModel and is defined like this:
private Customer _selectedRow;
...
public Customer SelectedRow
{
get { return _selectedRow; }
set
{
_selectedRow = value;
RaisePropertyChanged("SelectedRow");
}
}
...
Because of this I have not defined any DC on the CustomerSearchDetailView - it is set in the Binding on the "Master" view (as shown above) and it seems to work ok.
In my Model folder I have created the Customer class that is in use here. It implements ObservableObject and IDataErrorInfo and have public properties that raisepropertychanged events.
I run the application and everything seems to be ok. Note: the ViewModel for the CustomerSearchDetailView (that is CustomerSearchDetailViewModel.cs) is at this stage just an empty shell and not in use (as far as I can see ... the constructor is never accessed)
Now I want to add Save/Update functionality to my customer in the detail view. Ok, I add a Save button to the CustomerSearchDetailView like this:
<Button Content="Save" Command="{Binding Path = SaveCommand}" Width="80" Margin="0,0,15,0"/>
I create my "SaveCommand" RelayCommand property in my CustomerSearchDetailViewModel - but it is never accessed.
Hmmmmm ... well after some googling back and forth I come up with this:
<Button Content="Save" Command="{Binding Source={StaticResource MyCustDetails}, Path = SaveCommand}" Width="80" Margin="0,0,15,0"/>
I defined the "MyCustDetails" as a resource in this view pointing to the CustomerSearchDetailViewModel. And voila! I now hit the method when debugging ... but alas, my customer was of course "null". (In fact I spent 2 hours implementing the CommandParameter here and binding it to the "SelectedRow" Property on the master view - but the customer was still "null").
More googling and searching for mvvm examples, and I implemented my "SaveCommand" on the Customer class (the model object). And guess what? The edited customer got passed along - I could send it to my EF layer and everything seems to be ok ....
And - If you are still with me - here comes my questions:
1.) I would like - and thought that was the "proper MVVM way" of doing things - to have my CRUD/Repository accessing in the ViewModel. How can I do that in my scenario?
2.) Now that I have my CRUD in place via the Model class (Customer) - should i bother with question 1? In fact I have deleted the CustomerSearchDetailViewModel and everything runs ok. I feel I have invented the View - Model (MV) framework ... :-P
I would very much like feedback on this - and I apologize for this "wall of text".
Assuming DC means DataContext
Just my opinion:
First question is are you doing anything special with SelectedRow in CustomerSearchResultViewModel?
If the answer is no, just get rid of that property and have your CustomSearchDetailView bind directly to the DataGrid using {Binding ElementName=CustomerList, Path=SelectedItem}
Now your Save / update Commands need to be used by Button's in CustomerSearchDetailView. So instantly I'd be inclined to using a separate VM for that View and have these Command's defined there.
Now you mentioned these Commands were not accessed. Well the answer for that is because in your program you're never actually creating the CustomerSearchDetailViewModel.
Normal operation is your View's DataContext is it's VM(If it requires one. In your case you do imo cos you need it to hold your Commands)
looking at your code I'd guess your using MVVM Light. So in ViewModelLocator you have your Main property and in your Main View, you got the DataContext set using that Main property and Source={StaticResource Locator} where Locator is the ViewModelLocator created in App.xaml Resources. This thereby creates that ViewModel for that view defining that DataContext. You can ofcourse do the same in code-behind but let's not go off topic.
So in your case you got the DataContext set as SelectedRow which is of type Customer and Binding's are resolved using DataContext and that's why when your command's are defined in Customer it works fine but when it's in the VM it did not.
So why did it work when you had the commands in your VM and used
<Button Content="Save" Command="{Binding Source={StaticResource MyCustDetails}, Path = SaveCommand}" Width="80" Margin="0,0,15,0"/>
^^ That worked because the DataContext was not used since Source has been specified explicitly. and where-ever MyCustDetails was defined in resources, there the VM got created.
So it worked what's wrong with that?
Well it's quite a big mess. Also just like you mentioned Customer details in that VM was null. Well I hope you can guess why that was by now. It's because your VM was created in resources via x:Key="MyCustDetails" but nothing in it was ever used or set apart from when the Binding's referred to it explicitly
In this system we got commands that refer either to the Model which is plain wrong or the VM which is created as a resource just for this purpose. The DataContext is heavily linked to the "SearchResults" view making it not so easy for future extensions or layout updates.
If we keep the View <-> VM a 1 <-> 1 relattion we can avoid all this confusion. So in summary we can answer both your question's together. While this works, please don't let your code be like this and tweak it to better help expansion for future and comply with some basic guidelines.
So how do we do that?
Approach 1:
In your CustomerSearchDetail View, add a DependencyProperty of type Customer lets call this say SelectedCustomer.
Now replace DataContext="{Binding SelectedRow}" with SelectedCustomer="{Binding SelectedRow}" in CustomerSearchResultView
Now set the DataContext of your CustomerSerachDetailView as it's VM similar to how CustomerSerachResultsView links to it's VM(guessing through DataContext Binding in xaml using the ViewModelLocator)
Now you can have your commands in Button's of CustomerSerachDetailView just as <Button Command="{Binding SaveCommand}" ...
Finally because SelectedRow is no longer the DataContext of the CustomerSerachDetailsView, your Bindings for FirstName, Lastname, Address will all appear to stop working.
We got plenty of options to address this.
First is to in each Binding use a RelativeSource FindAncestor binding pointing to CustomerSerachDetailsView and there via the CurrentCustomer DP(DependencyProperty) we created before get the appropriate field.
eg:
<TextBlock Text={Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:CustomerDetailsView}}, Path=CurrentCustomer.FirstName}" />
now if you have multiple properties this is gonna soon start getting annoying to type. So then pick a common ancestor(say 3 of these TextBlocks are grouped under a StackPanel) and apply it's DataContext as the CurrentCustomer element via a similar binding to ^^. Now the StackPanel's children DataContext will be the Customer element so in each of their binding's you don't have to do the whole RelativeSource thing and can just mention {Binding Path=FirstName} and so on.
That's it. Now you got two view's with their own respective VM and a Model(Customer) and each have their respective tasks.
Great, we done? err not quite yet.
While Approach 1 is better than what we started with it's still just "meh". We could do better.
Approach 2
MVVMLight has a Messenger class that will allow you to communicate between different classes in a weak dependent format. You need to look into this if you haven't already.
So what do we do with Messenger?
pretty simple:
In the setter of SelectedRow in CustomerSearchResultsViewModel we'll send a message with the new incoming value to CustomerSearchDetailsViewModel.
Now in CustomerSearchResultsViewModel we'll add a property CurrentCustomer and assign it this incoming value.
In the CustomerSerachDetailsView we no longer create a DP. Which means we no longer set SelectedRow to anything(DataContext or DP) in the CustomerSerachDetailsView from CustomerSearchResultsView ( sweet less work :) )
As for the way we assign DataContext of CustomerSerachDetailsView or way we bind the Button.Command - They remain same as Approach 1
Finally the actual "FirstName" and so Binding's. Well now CurrentCustomer is a property of the CustomerSearchDetailsViewModel. So binding to it just like how the Button bind's to it's commands
^^ this works fine now cos DataContext for the TextBlock is the VM and the property CurrentCustomer exists in it.
How can I bind a Text property for my TextBox that read from a source but it will store its value to a different target?
Let's say
I have a textbox which is bond to a path in a CollectionViewSource
<Window>
<Window.Resources>
<CollectionViewSource Source="{Binding Source={StaticResource ProgramView}, Path='FK_LevelList_ProgramList'}" x:Key="LevelLookupView" />
</Window.Resources>
<TextBox Name="FeePerTermTextbox" Text="{Binding Source={StaticResource LevelLookupView}, Path='FeePerTerm', Mode=OneWay, StringFormat=c2}"/>
</Window>
When perform save, the value of the TextBox will store to another model that is different from the CollectionViewSource
Thanks
I consider this flawed. What happens if the source gets updated? Should the textbox be overwritten?
The reason for this design is imho, that the UI should reflect the "traits" of the element set as DataContext, therefore i expect it to contain the value i give in the model or in the ui. Now there is of course nothing stopping you from not writing the value in your viewmodel to your model, when receiving the set value from the textbox.
public class Redirecter
{
public string FileName
{
get{return mModel.FileName;}
set{mProxy.FileName = value;}
}
}
But this of course won't work well together with INotifyPropertyChanged. I would use a different approach. Use a model that reflects your ui more. If you open the view fill in this ui model with your settings from model A. If you now save this, save each property into Model B.
I have a Silverlight control that has my root ViewModel object as it's data source. The ViewModel exposes a list of Cards as well as a SelectedCard property which is bound to a drop-down list at the top of the view. I then have a form of sorts at the bottom that displays the properties of the SelectedCard. My XAML appears as (reduced for simplicity):
<StackPanel Orientation="Vertical">
<ComboBox DisplayMemberPath="Name"
ItemsSource="{Binding Path=Cards}"
SelectedItem="{Binding Path=SelectedCard, Mode=TwoWay}"
/>
<TextBlock Text="{Binding Path=SelectedCard.Name}"
/>
<ListBox DisplayMemberPath="Name"
ItemsSource="{Binding Path=SelectedCard.PendingTransactions}"
/>
</StackPanel>
I would expect the TextBlock and ListBox to update whenever I select a new item in the ComboBox, but this is not the case. I'm sure it has to do with the fact that the TextBlock and ListBox are actually bound to properties of the SelectedCard so it is listening for property change notifications for the properties on that object. But, I would have thought that data-binding would be smart enough to recognize that the parent object in the binding expression had changed and update the entire binding.
It bears noting that the PendingTransactions property (bound to the ListBox) is lazy-loaded. So, the first time I select an item in the ComboBox, I do make the async call and load the list and the UI updates to display the information corresponding to the selected item. However, when I reselect an item, the UI doesn't change!
For example, if my original list contains three cards, I select the first card by default. Data-binding does attempt to access the PendingTransactions property on that Card object and updates the ListBox correctly. If I select the second card in the list, the same thing happens and I get the list of PendingTransactions for that card displayed. But, if I select the first card again, nothing changes in my UI! Setting a breakpoint, I am able to confirm that the SelectedCard property is being updated correctly.
How can I make this work???
If you are using Silverlight 3 you will need to use INotifyPropertyChanged.
Example:
public class CardViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Card> Cards { get; set; }
private Card _selectedCard;
public SelectedCard
{
get
{
return _selectedCard;
}
set
{
if (value != _selectedCard)
{
_selectedCard = value;
NotifyPropertyChanged("SelectedCard");
}
}
}
public CardViewModel()
{
Cards = new ObservableCollection<Card>();
//Populate Cards collection with objects
}
public void NotifyPropertyChanged(string item)
{
if (PropertyChanged!=null)
{
PropertyChanged(this, new PropertyChangedEventArgs(item));
}
}
}
All you would need to do is set this class to your views DataContext and everything should be happy.
A pattern I've been using recently is to bind the data context of a container of detail info to the selected item of the list box. The XAML in your case becomes:
<StackPanel Orientation="Vertical">
<ComboBox x:Name="_lbxCards" <-- new
DisplayMemberPath="Name"
ItemsSource="{Binding Path=Cards}"
SelectedItem="{Binding Path=SelectedCard, Mode=TwoWay}"
/>
<StackPanel DataContext={Binding ElementName=_lbxCards,Path=SelectedItem}> <-- new
<TextBlock Text="{Binding Path=Name}" <-- updated
/>
<ListBox DisplayMemberPath="Name"
ItemsSource="{Binding Path=PendingTransactions}" <-- updated
/>
</StackPanel> <-- new
</StackPanel>
Turns out the problem isn't in the UI at all. The PendingTransactions class lazy-loads its values using a async WCF call to the server. The async pattern uses events to notify the caller that the operation is complete so the data can be parsed into the class. Because each Card has its own instance of the PendingTransactions class and we used a ServiceFactory to manage our WCF proxies, each instance was wiring up their event handler to the same event (we are using a singleton approach for performance reasons - for the time being). So, each instance received the event each time any of the instances triggered the async operation.
This means that the data-binding was working correctly. The PendingTransactions collections were overwriting themselves each time a new Card was viewed. So, it appeared that selecting a previous card did nothing when, in fact, it was selecting the correct object for binding, it was the data that was screwed up and make it look like nothing was changing.
Thanks for the advice and guidance nonetheless!