I still consider myself a beginner when it comes to MVVM and I am having an issue with a binding in a TabControl. My application allows the user to create nations and states, input some information and then save them to a database. Below is a descriptions of how my application is structured:
The base ViewModel/View is called ApplicationViewModel/ApplicationView. The ApplicationViewModel has an ObservableCollection called Tabs consisting of one AllNationsViewModel and one AllStatesViewModel. This Tabs property is bound to the TabControl's ItemsSource in the ApplicationView.
AllNationsViewModel/AllNationsView are used to display all the nations that have been entered by the user. It also allows the user to create new nations and select a particular nation for closer inspection. The AllStatesViewModel/AllStatesView do the same but for states.
Finally I have the NationViewModel/NationsView that deals with a particular nation; also here there are StateViewModel/StateView that do the same for a state.
For a nation you can at the moment only input a name but for a state you can input a name as well as the nation it is part of. The nation is selected using a ComboBox where all the nations created in the nations tab show up.
I use a static class called DataFacade as an interface to my data store. It is possible to add, remove and retrieve a list of nations/states using this interface; also it triggers events when something is added or removed.
The problem I am having is that when there is a state selected in the AllStatesViewModel (CurrentStateViewModel property) and I go to the nations tab and then back to the states tab the currently selected state has lost its nation. All the other states are still ok tough.
I will try to show the relevant code below (I have removed irrelevant code from some methods):
State class:
class State
{
public string Name { get; set; }
public Nation Nation { get; set; }
}
TabControl in the ApplicationView:
<TabControl ItemsSource="{Binding Tabs}" Margin="6">
<TabControl.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding DisplayName}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding }" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
When the user creates a nation the AddCommand of the AllNationsViewModel is triggered:
private void Add(NationViewModel vm)
{
DataFacade.AddNation(vm.Nation);
}
The AllStatesViewModel gets notified when a nation gets added to the data store:
private void OnDataStoreNationsChanged(object sender, DataFacadeEventArgs e)
{
Nations.Add(new NationViewModel(e.Context as Nation));
}
The Nations property above is an ObservableCollection of NationViewModels. Now, this property is used by the ComboBox in the StateView to populate its items so a nation can be selected when creating/editing a state:
<ComboBox Grid.Row="1" Grid.Column="1" SelectedValue="{Binding Nation}" SelectedValuePath="Nation" ItemsSource="{Binding DataContext.Nations, RelativeSource={RelativeSource AncestorType={x:Type local:AllStatesView}}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
I believe the problem has to do with the binding above. Because if I don't bind to the Nations property in AllStatesViewModel but instead bind to a temporary property in the ApplicationViewModel everything works. Can it be that the AllStatesView is thrown away by the TabControl when I go to another tab therefor the binding above sets the Nation property of my StateViewModel to null? When I debug I see that I get a null when exiting the states tab.
How would someone who is not a beginner like me solve this situation? I find my temporary solution rather ugly. I am not entirely sure how I should handle the data store access since all the MVVM examples I have found don't focus on this part.
EDIT: Addded some pictures as requested:
Atm I just have the simplest testing GUI set-up:
Here you see the AllNationsView, atm the NationView is only the "Name" TextBlock and the TextBox at the top.
Here is the AllStatesView, at the top is the currently selected state (displayed using StateView). Where you now see that USA is selected as nation for Montana, if I go to the Nations tab and then back to the States tab the nation for Montana is now blank. If I select Florida it still has USA as its nation.
WPF only keeps the UI for the active tab in memory. When you change tabs that UI is destroyed and the UI of the new tab is created and rebound.
There are a few ways around the problem you are having. You can use the Repository Pattern to store and access your data sources separate from the view models. Basically, an outside object holds your data sources, such as the lists of states and nations. That way they don't get destroyed when the active tab changes.
The other option is to store the data sources on your ApplicationViewModel and access them via a references to the ApplicationViewModel on each individual tab's view model. You shouldn't have to use a RelativeSource binding anywhere in that.
Related
i have a window which loads a Customer table and another user control of input fields, when i select i wish to populate the user control inputs. I currently have a datagrid that a selected item is set in the CustomerViewModel through binding. When this is selected it updates a textbox with the selected items property such as name, email, etc. I have a CustomerSettingsViewModel which contains multiple input fields. I am trying to bind the selected item to inputs within this model, however as the CustomerViewModel doesn't know about the CustomerSettingsViewModel i cant see the binds within the textbox inputs.
The views are loaded using DataTemplate using the datatype.
MainWindow.xaml
<Window.Resources>
<DataTemplate DataType="{x:Type VM:CustomerVM}">
<View:Customers/>
</DataTemplate>
<DataTemplate DataType="{x:Type VM:CustomerSettingsVM}">
<View:CustomerSettings />
</DataTemplate>
<DataTemplate DataType="{x:Type VM:SuppliersVM}">
<View:Suppliers/>
</DataTemplate>
<DataTemplate DataType="{x:Type VM:SuppliersSettingsVM}">
<View:SupplierSettings/>
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding SelectedMain}" Margin="0,135,0,10" Grid.ColumnSpan="2"/>
<ContentControl Content="{Binding SelectedSettings}" Margin="105,53,10,45"/>
These are set and get the correct views depending on the datatype that being presented.
MainVM.cs
Customer = new CustomerVM();
CustomerSettings = new CustomerSettingsVM();
SelectedMain = Customer;
SelectedSettings = CustomerSettings;
within the CustomerVM i have a get and selected with binds to anything within the customer view, however how can i get the customer settings view to see the selected customer has changed and populate the inputs?
CustomerVM.cs
public Customer SelectedCustomer
{
get { return _selectedCustomer; }
set
{
_selectedCustomer = value;
RaisePropertyChangedEvent("SelectedCustomer");
}
}
i have upload a simple solution of my problem onto GitHub Might give a better understanding of what i am trying to achieve
I think you missed a few concepts about MVVM, maybe you should go back to basics.
Basically, your viewmodels have to be a "testable copy" of your view.
So if your target is to build a Customer View containing Customer Settings, what you need is:
a CustomerViewModel with a CustomerSettingsViewModel property
a CustomerView using CustomerViewModel as datacontext
a CustomerSettingsView declared into your CustomerView binded to the CustomerSettingsViewModel from the CustomerViewModel
Another way to put this: if you want a view to contain another view, you can have a viewModel to contain another viewModel.
This other question could show you how to use a vm as a property of another vm.
Please see my proposed solution using your GitHub example. Basically your problem is that you should not use directly the Customer Model in a View, but create a CustomerVm instead, and just delete the CustomerSettingVm.
You might understand better my implementation by reading how I'm used to deal with MVVM.
Hope it helps.
I am quite new to WPF development, and currently I am trying to use the MVVM on my application development. I have read a lot about MVVM navigation and switching views, but I can't find a solution for my current situation. Let's explain what it is:
First of all, I have my main View element, a Dockpanel, with some fixed areas, and a main "dynamic" area where the content should change, depending on actions:
<DockPanel>
<Label Content="Top Fixed element"/>
<StackPanel Orientation="Vertical" Height="auto" Width="150" DockPanel.Dock="Left">
<Label Content="SomeOptions"/>
<!-- some more elements -->
</StackPanel>
<Label DockPanel.Dock="Bottom" Content="Foot"/>
<ContentControl Content="{Binding CurrentMainViewElementViewModel}"/>
</DockPanel>
I have defined some DataTemplates that I would like to load in this ContentControl, here there is one of the Data Templates as example:
<Window.Resources>
<DataTemplate DataType="{x:Type ViewModel:FileLoaderVM}">
<View:FileLoaderView/>
</DataTemplate>
</Window.Resources>
This FileLoader (View and View Model are implemented, using the RelayCommand and the INotifyPropertyChanged) opens a dialog box after clicking a button, where after selecting a file it is opened and parsed, and show all the found elements inside a ListView with multiple selection(in this case, persons with their data).
What I want to do now is to load another user control in this ContentControl, when I click a button. This button is defined in my view model like this:
public ICommand LoadPersons
{
get { return new RelayCommand(param => this.loadSelectedPersons(), param => (SelectedPersons!=null && SelectedPersons.Any()));}
}
My question comes at this point, how can I modify the content of the ContentControl, loading another User Control instead of the current one directly from my view model (in this "this.loadSelectedPersons()")?
If this is not possible, how should I approach to solve this problem?
Next to this action, I want to show all the previously selected elements and manipulate in different possible ways (inserting in a DB, saving in another file and so on), and I have already for that the appropriate User Control, that I would like to show in my main view element in the ContentControl section, keeping the other elements as they are originally.
lets see if i get you right.
you have a mainviewmodel with a property (CurrentMainViewElementViewModel) bound to the ContentControl. your MainViewmodel set the FileLoaderVM to this Property. now you wanna show a "new/other" Viewmodel when a File is seleted in your FileLoaderVM?
why dont you simply expose a event from your FileLoaderVM and subscribe to this event in your MainViewModel? if you do so your MainViewModel can then set the "new/other" Viewmodel to the ContentControl
To change content of ContentControl you do not load another user control, but change value of CurrentMainViewElementViewModel (to which ContentControl.Content is bound) to a new ViewModel, which will load another UserControl (defined in DataTemplate same way as FileLoaderVM is).
This looks like a job for main ViewModel (where CurrentMainViewElementViewModel is located).
Easiest solution is to provide a method in that ViewModel
public Switch()
{
CurrentMainViewElementViewModel = SomeViewModel;
}
and call this method from FileLoaderVM.
I have a basic project in WPF.
All it does it retrieve / update products.
As shown in the image below, the user enters an ID, the data is then displayed according to it, and the user is able to change the data and click 'Save Product' to save it to the database.
The GetProduct(int id) function retrieves a product by the ID provided.
The SaveProduct() function saves the changed fields.
Also, there are two DataTemplates:
1) For the ProductModel - includes 3 textboxes: ProductId, ProductName, UnitPrice.
2) For the ProductViewModel - includes the save/get buttons + a textbox for the user to enter the id of the desired product.
What I'm trying to do is get the changed data when a user clicks the 'Save Product' button.
The most ideal way in my opinion, is to use Binding.
Each textbox is already binded, but I have no idea how to get the binded data.
Here is an example of a binded textbox in the FIRST DataType (ProductModel):
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ProductId}" Margin="5" Width="150" />
There is one for each of the following properties: ProductId, ProductName and UnitPrice.
IMPORTANT!: The Get/SaveProduct() functions are in the ProductViewModel class, while the actual product class is - you guessed it - ProductModel. The ProductViewModel class holds a variable that contains the current product displayed.
This is the button that's used to save the info - it is written in the SECOND DataType (ProductViewModel):
<Button Content="Save Product" DockPanel.Dock="Right" Margin="10,2" VerticalAlignment="Center" Command="{Binding Path=SaveProductCommand}" Width="100" />
The SaveProductCommand command simply fires the SaveProduct() function.
I have a few questions regarding this whole subject:
What does it mean when a binding is used like this : {Binding ProductId} ?
The default binding mode for textboxes is TwoWay as far as I remember. But in this case, ProductId/Name + UnitPrice are not dependency properties, therefore is it right that the binded values do not update/sent back when the text in the textboxes is changed? (Since there isn't an event attached to it...)
A data context was never configured in my project, but all of the "binding tags" in my XAML pages don't seem to have a defined source. Could it be that the source is actually the DataType in the DataTemplate that includes the binded objects?
The SECOND DataTemplate (the ProductViewModel one) has this ContentControl tag: <ContentControl Margin="10" Content="{Binding Path=CurrentProduct}" />.
What is it's purpose?
If a TwoWay binding were/does occur, how do I get the values from within the SaveProduct() function? Do I just refer to, say CurrentProduct.ProductName to get the changed name?
Much thanks to everyone who takes their time to answer - I appreciate it so much!
What does it mean when a binding is used like this : {Binding
ProductId} ?
The specific control property you have this binding set on is going to look for the ProductId property on the object set as the DataContext and set the propertys value in the control accordingly.
The default binding mode for textboxes is TwoWay as far as I remember.
But in this case, ProductId/Name + UnitPrice are not dependency
properties, therefore is it right that the binded values do not
update/sent back when the text in the textboxes is changed? (Since
there isn't an event attached to it...)
You do not need to make the properties within your object a DependencyProperty for TwoWay binding to occur.
A data context was never configured in my project, but all of the
"binding tags" in my XAML pages don't seem to have a defined source.
Could it be that the source is actually the DataType in the
DataTemplate that includes the binded objects?
The bindings being set within your XAML will use the object stored within the DataContext, thus if you do not explicitly set the DataContext of the view, it will be null. You should note however that the DataContext is inherited from its parent. If you are in fact setting the content by using say, CurrentProduct, then all the properties will be available to bind to per your Product type.
The SECOND DataTemplate (the ProductViewModel one) has this
ContentControl tag:
<ContentControl Margin="10" Content="{Binding Path=CurrentProduct}" />
What is it's purpose?
It is acting as the container of your CurrentProduct, which can contain one and only one item.
If a TwoWay binding were/does occur, how do I get the values from
within the SaveProduct() function? Do I just refer to, say
CurrentProduct.ProductName to get the changed name?
Without seeing the entire application, my guess is that the ContentControl is being set to the CurrentProduct and your TextBox, etc.. are all bound to the respective properties, such as CurrentProduct.ProductId, etc... The product which you want to save is in fact the CurrentProduct. When you call save within your ViewModel, you simply access the CurrentProduct and persist it as needed, where CurrentProduct.PropertyName will contain the changes which were propagated from the UI.
I am new to MVVM, I have a checkedlistbox in a view with the list of titles(have bound the exposed property in ViewModel to this checkedlistbox control)...
Here is my XAML code that populates the ListCheckBox -
<ListBox x:Name="lstCode" ItemsSource="{Binding Code,Mode=TwoWay}" Grid.Row="1" Style="{StaticResource ListBoxStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox x:Name="chkBox" IsChecked="{Binding IsChecked,Mode=TwoWay}" Content="{Binding Code_Name}" Margin="0" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This control shows the correct list of items with checkboxes for each item in the listbox...
What should be the code in viewmodel to make it work in two way - while getting the codes from database, it should automatically selected the code from the listcheckedbox and when the user selects one or more codes, the viewmodel should be able to know the items selected...
In general, for TwoWay binding, you will need to implement the INotifyPropertyChanged interface on the ViewModel you want to bind to.
In this case, your ViewModel will have to provide a property that returns a collection that your view can bind to, e.g. an ObservableCollection.
This ObservableCollection already allows you to add, update, and delete items in that list in a way that automatically communicates the changes between View and ViewModel.
For the rest I suggest to start digging into MVVM depths. To fully take advantage of WPF's capabilities, you will need to understand the basics for yourself. A great starting point is this SO thread: MVVM: Tutorial from start to finish?
I have a ObservableCollection that's bound to a ListBox in WPF. I want the ListBox to be editable, and for the editing changes to be saved to the collection. Since WPF doesnt provide an editable listbox, I've tried creating my own by changing the ListBox.ItemTemplate.
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Name="EditableText" Text="{TemplateBinding Content}"/>
</DataTemplate>
</ListBox.ItemTemplate>
Changing the ItemTemplate gives me editable boxes, but any changes to the textboxes dont get saved to the ObservableCollection. Is there a way to have an editable ListBox with two way binding?
You cannot do it this way.
To achieve that kind of trick, you would need your items to be "holder classes" that expose a property you can bind your textbox to.
To understand it, imagine the following pseudo sequence of calls:
class ListBox
{
Bind(Items)
{
foreach(var item in Items)
{
DataTemplate Template = LoadTemplateForItem(item.GetType()); // this is where your template get loaded
Template.Bind(item); //this is where your template gets bound
}
}
}
Your template (the DataTemplate with the listbox) is loaded and the item (which I assume is a string in your case) gets passed in.
At this point, it only knows the string, and cannot influence anything upwards. A two-way binding cannot influence the collection because the template does not know in which context it is being used, so it cannot reach back to the original collection and modify its contents.
For that matter, this is the same thing for the TextBox. If it is not given a conainer and a property name, it has nowhere to "store back" the changes.
This basically the same as passing a string into a function call. The function cannot change which string was passed in (ignoring tricks such as by-reference argument passing).
To get back to your case, you need to build a collection of objects which expose a property containing the value that needs to be edited:
public class MyDataItem
{
string Data { get; set;}
}
Then you can bind your ListBox to a collection of those items and modifiy your datatemplate:
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Name="EditableText" Text="{Binding Data, Mode=TwoWay}"/>
</DataTemplate>
</ListBox.ItemTemplate>
Bind to a model property -- i.e. a property of the data object -- rather than to a view property such as Content. For example:
// model class
public class Widget : INotifyPropertyChanged
{
public string Description { ... }
}
<!-- view -->
<DataTemplate>
<TextBox Text="{Binding Description}" />
</DataTemplate>
Note this will not work if your ItemsSource is ObservableCollection (because there's no property to bind to).