I am new to MVVM and WPF and I am a little bit confused.
Let's say I have 2 models, like this:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
and
public class Product
{
public int Id { get; set; }
public Category Category { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
I am not sure what is the recommended way of implementing the ProducViewModel class for adding a new Product:
Is it OK to just use a public property of type Product and bind it to the view, like this:
public class ProductViewModel : Screen
{
private Product _product;
public BindableCollection<Category> Categories;
public ProductViewModel(Product product, BindableCollection<Category> categories)
{
_product = product;
Categories = categories;
}
public Product Product
{
get { return _product }
set
{
_product = value;
SetAndNotify(ref _product, value);
}
}
}
In this case I will use the Category collection in the form to choose a category.
My view should look something like this:
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Label Content="Catgory" />
<ComboBox
ItemsSource="{Binding Categories}"
DisplayMemberPath="Name"
SelectedValue="?????? What comes here? "/>
</StackPanel>
</StackPanel>
Is this correct?
And also, how I will bind Product.Category in a ComboBox in the view?
Thanks!
In your View Model you should expose properties that are needed in your view. This case you can bind directly to your Product.Category or create a new property in your view model, and i think this last option is well suited in a MVVM scenario.
In your case you should create a selected category property your ProductViewModel:
public Category SelectedCategory { get { return _selectedCategory; }
set
{
_selectedCategory = value;
SetAndNotify(ref _selectedCategory, value);
Product.Category = _selectedCategory;
} }
And then in your xaml, you bind combobox's selected value to this new property:
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Label Content="Catgory" />
<ComboBox
ItemsSource="{Binding Categories}"
DisplayMemberPath="Name"
SelectedValue="{Binding SelectedCategory}"/>
</StackPanel>
Related
I am new to WPF application development. I want to get floors on the basis of blocks. I have one combo box of blocks and another combo box of floors. When I select any block in one combo box, the other combo box should display the floors of the select block.
This is the combo box layout:
<ComboBox Grid.Row="0" Grid.Column="0" Width="100"
Margin="0,0,0,10" Height="35"
Loaded="FrameworkElement_OnLoaded"
SelectedValuePath ="Id"
SelectedValue ="{Binding SelectedBlockId, Mode=TwoWay}"
DisplayMemberPath="Name"
SelectionChanged="Selector_OnSelectionChanged" ItemsSource="{Binding Blocks}" />
<ComboBox Grid.Row="0" Grid.Column="3" Width="100"
Margin="0,0,0,10" Height="35"
Loaded="FrameworkElement_OnLoaded"
SelectedValuePath ="Id"
SelectedValue ="{Binding SelectedFloorId, Mode=TwoWay}"
DisplayMemberPath="Name"
SelectionChanged="Selector_OnSelectionChanged" ItemsSource="{Binding Floors}" />
Here's a slightly different example.
Xaml...
<!-- language: xaml -->
<Label Name="FavoriteFoodLbl" Grid.Column="0" Grid.Row="13">Favorite Food</Label>
<ComboBox Name="FavoriteFoodCombo" Grid.Column="1" Grid.Row="13" ItemsSource="{Binding Foods}" SelectedItem="{Binding FavoriteFood, UpdateSourceTrigger=PropertyChanged}" />
<Label Name="FavoriteFlavourLbl" Grid.Column="2" Grid.Row="13">Favorite Flavour</Label>
<ComboBox Name="FavoriteFlavourCombo" Grid.Column="3" Grid.Row="13" ItemsSource="{Binding Flavours}" />
View model/code-behind code...
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<string> Foods { get; set; } = new ObservableCollection<string>() { "Pizza", "Ice Cream", "Soup" };
private string _favoriteFood;
public string FavoriteFood
{
get { return _favoriteFood; }
set
{
_favoriteFood = value;
switch (_favoriteFood)
{
case "Pizza":
_flavours = new ObservableCollection<string>(PizzaToppings);
break;
case "Ice Cream":
_flavours = new ObservableCollection<string>(IceCreamFlavours);
break;
case "Soup":
_flavours = new ObservableCollection<string>(SoupFlavours);
break;
default:
_flavours = new ObservableCollection<string>();
break;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("FavoriteFood"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Flavours"));
}
}
public List<string> PizzaToppings { get; set; } = new List<string>() { "Margarita", "Pepperoni", "Meat Feast" };
public List<string> IceCreamFlavours { get; set; } = new List<string>() { "Vanilla", "Strawberry", "Chocolate" };
public List<string> SoupFlavours { get; set; } = new List<string>() { "Tomato", "Leek and Potato", "Chicken" };
private ObservableCollection<string> _flavours = null;
public ObservableCollection<string> Flavours
{
get
{
return _flavours;
}
set
{
_flavours = value;
RaisePropertyChanged();
}
}
public string FavoriteFlavour { get; set; }
Change out the case statement for something appropriate.
Does this help?
What you need to do is to establish master-detail relationship between Block and Floor models and correctly bind them to you View (comboboxes).
So supposing your Floor and Block have two properties ID and Description, your model should look like this:
public class Floor
{
public int Id { get; set; }
public string Description { get; set; }
}
public class Block
{
public int Id { get; set; }
public int Description { get; set; }
// notice Floors collection inside each block
public IList<Floor> Floors { get; set; }
public Block()
{
Floors = new List<Floor>();
}
}
Your ViewModel will contain two ObservableCollections and one property to store currently selected block. Notice SelectedBlock property setter: when the property is updated, the Floors collection gets recreated with new values.
public const string BlocksPropertyName = "Blocks";
private ObservableCollection<Block> _blocks = null;
public ObservableCollection<Block> Blocks
{
get
{
return _blocks;
}
set
{
_blocks = value;
RaisePropertyChanged(BlocksPropertyName);
}
}
public const string SelectedBlockPropertyName = "SelectedBlock";
private Block _selectedBlock = null;
public Block SelectedBlock
{
get
{
return _selectedBlock;
}
set
{
_selectedBlock = value;
RaisePropertyChanged(SelectedBlockPropertyName);
if (_selectedBlock != null)
{
Floors = new ObservableCollection<Floor>(_selectedBlock.Floors);
}
}
}
public const string FloorsPropertyName = "Floors";
private ObservableCollection<Floor> _floors = null;
public ObservableCollection<Floor> Floors
{
get
{
return _floors;
}
set
{
_floors = value;
RaisePropertyChanged(FloorsPropertyName);
}
}
In your XAML you just bind both ComboBoxes to corrispective collections:
<ComboBox ItemsSource="{Binding Blocks}"
SelectedItem="{Binding SelectedBlock}" />
<ComboBox ItemsSource="{Binding Floors}" />
Please refer to the following blog post for an example of how to implement cascading ComboBoxes in WPF using the MVVM pattern: https://blog.magnusmontin.net/2013/06/17/cascading-comboboxes-in-wpf-using-mvvm/
You could basically just replace the Countries and the Cities types from the sample code with your Block and Floor types.
The View is:
<Controls:SplitButton Margin="217,409.75,56,185" Name="SplitButton1"
Width="384"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Orientation="Vertical"
DisplayMemberPath ="UserName"
SelectedItem="{Binding SelectedUser,UpdateSourceTrigger=PropertyChanged,Mode=OneWay}"
ItemsSource="{Binding Users, Mode=TwoWay}" />
The ViewModel is:
public string SelectedUser
{
get { return selectedUser; }
set
{
selectedUser = value;
RaisePropertyChanged("SelectedUser");
}
}
public ObservableCollection<UserModel> Users
{
get
{
return users;
}
set
{
users = value;
}
}
the Model is:
public class UserModel
{
private int id;
private string userName;
private int groupId;
private string deviceMacAddress;
public int Id { get; set; }
public string UserName { get; set; }
public int GroupId { get; set; }
public string DeviceMacAddress { get; set; }
}
i use the above code in xaml to bind the selectedItem in the splitbutton to ViewModel->property--SelectedUser.
but it does not work. anyone knows why?
SelectedUser is returned as Model name (PresentationLayer.Model.UserModel) instead of UserName prooperty.
Because your binding is OneWay by your definition.
Set your binding to TwoWay.
<Controls:SplitButton SelectedItem="{Binding SelectedUser,Mode=TwoWay}"/>
And, there is no need to set the UpdateSourceTrigger=PropertyChanged in this case, because the UpdateSourceTrigger is PropertyChanged by default for the SelectedItem property.
I have a ListBox within a DataTemplate of another ListBox simplified to the following XAML
<ListBox ItemsSource="{Binding MovieList}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<ListBox ItemsSource="{Binding Cast}"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedCastMember, Mode=TwoWay}"/>
<TextBlock Text="{Binding MovieName}"/>
<TextBlock Text=....../>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The Parent List is bound to an ObservableCollection of Movie with each Movie object in turn having an ObservableCollection of cast members bound to a list box. The ViewModel property and Class below
public const string MovieListPropertyName = "MovieList";
private ObservableCollection<Movie> _movieList;
public ObservableCollection<Movie> MovieList
{
get
{
return _movieList;
}
set
{
if (_movieList == value)
{
return;
}
RaisePropertyChanging(MovieListPropertyName);
_movieList = value;
RaisePropertyChanged(MovieListPropertyName);
}
}
public const string SelectedCastMemberPropertyName = "SelectedCastMember";
private MovieCastMember _selectedCastMember;
public MovieCastMember SelectedCastMember
{
get
{
return _selectedCastMember;
}
set
{
if (_selectedCastMember == value)
{
return;
}
RaisePropertyChanging(SelectedCastMemberPropertyName);
_selectedCastMember = value;
RaisePropertyChanged(SelectedCastMemberPropertyName);
}
}
With the Movie and MovieCastMember classes as follows
public class Movie
{
public int Id { get; set; }
public string Name { get; set; }
public int Year { get; set; }
public string Overview { get; set; }
public double VoteAverage { get; set; }
public ObservableCollection<MovieCastMember> Cast { get; set; }
public BitmapImage PosterImage { get; set; }
}
public class MovieCastMember
{
public int Id { get; set; }
public string Name { get; set; }
}
I want to select a cast member in any of the Movie Lists and bind the MovieCastMember object to a property in my ViewModel. My List boxes populates fine, I have tried various scenarios in XAML but the SelectedItem are not updating the property in the ViewModel. Any help would be appreciated.
That's because you're binding against Movie class in second listBox. There is no "SelectedCastMember". Move it to Movie class and it will work.
I have a sample MVVM WPF application and I'm having problems creating DataTemplates for my dynamically loaded model. Let me try explain:
I have the following simplified classes as part of my Model, which I'm loading dynamically
public class Relationship
{
public string Category { get; set; }
public ParticipantsType Participants { get; set; }
}
public class ParticipantsType
{
public ObservableCollection<ParticipantType> Participant { get; set; }
}
public class ParticipantType
{
}
public class EmployeeParticipant : ParticipantType
{
public EmployeeIdentityType Employee { get; set; }
}
public class DepartmentParticipant : ParticipantType
{
public DepartmentIdentityType Department { get; set; }
}
public class EmployeeIdentityType
{
public string ID { get; set; }
}
public class DepartmentIdentityType
{
public string ID { get; set; }
}
Here is how my View Model looks like. I created a generic object Model property to expose my Model:
public class MainViewModel : ViewModelBase<MainViewModel>
{
public MainViewModel()
{
SetMockModel();
}
private void SetMockModel()
{
Relationship rel = new Relationship();
rel.Category = "213";
EmployeeParticipant emp = new EmployeeParticipant();
emp.Employee = new EmployeeIdentityType();
emp.Employee.ID = "222";
DepartmentParticipant dep = new DepartmentParticipant();
dep.Department = new DepartmentIdentityType();
dep.Department.ID = "444";
rel.Participants = new ParticipantsType() { Participant = new ObservableCollection<ParticipantType>() };
rel.Participants.Participant.Add(emp);
rel.Participants.Participant.Add(dep);
Model = rel;
}
private object _Model;
public object Model
{
get { return _Model; }
set
{
_Model = value;
NotifyPropertyChanged(m => m.Model);
}
}
}
Then I tried creating a ListBox to display specifically the Participants Collection:
<ListBox ItemsSource="{Binding Path=Model.Participants.Participant}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Expander Header="IdentityFields">
<!-- WHAT TO PUT HERE IF PARTICIPANTS HAVE DIFFERENT PROPERTY NAMES -->
</Expander>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
The problem is:
I don't know how to create a template that can handle both type of ParticipantTypes, in this case I could have EmployeeParticipant or DepartmentParticipant so depending on that, the data binding Path would be set to Employee or Department properties accordingly
I though about creating a DataTemplate for each type (e.g. x:Type EmployeeParticipant) but the problem is that my classes in my model are loaded dynamically at runtime so VisualStudio will complain that those types don't exist in the current solution.
How could I represent this data in a ListBox then if my concrete types are not known at compile time, but only at runtime?
EDIT: Added my test ViewModel class
You can still create a DataTemplate for each type but instead of using DataType declarations to have them automatically resolve you can create a DataTemplateSelector with a property for each template (assigned from StaticResource in XAML) that can cast the incoming data item to the base class and check properties or otherwise determine which template to use at runtime. Assign that selector to ListBox.ItemTemplateSelector and you'll get similar behavior to what DataType would give you.
That's not a good view-model. Your view-model should be view-centric, not business-centric. So make a class that can handle all four cases from a visual perspective, then bridge your business classes over to that view-model.
EDIT:
Working off your code:
<ListBox ItemsSource="{Binding Path=Model.Participants}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Expander Header="IdentityFields">
<TextBlock Text={Binding Id} />
<TextBlock Text={Binding Name} />
</Expander>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I changed the binding, I assume that was a mistake?
I would create a ViewModel for Participant:
public class Participant_VM : ViewModelBase
{
private string _name = string.Empty;
public string Name
{
get
{
return _name ;
}
set
{
if (_name == value)
{
return;
}
_name = value;
RaisePropertyChanged(() => Name);
}
private string _id= string.Empty;
public string Id
{
get
{
return _id;
}
set
{
if (_id== value)
{
return;
}
_id = value;
RaisePropertyChanged(() => Id);
}
}
}
Modify the ListBox as follows.
<ListBox ItemsSource="{Binding Model.Participants.Participant}">
<ListBox.Resources>
<DataTemplate DataType="{x:Type loc:DepartmentParticipant}">
<Grid>
<TextBlock Text="{Binding Department.ID}"/>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type loc:EmployeeParticipant}">
<Grid>
<TextBlock Text="{Binding Employee.ID}"/>
</Grid>
</DataTemplate>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Expander Header="IdentityFields">
<!-- WHAT TO PUT HERE IF PARTICIPANTS HAVE DIFFERENT PROPERTY NAMES -->
<ContentPresenter Content="{Binding }"/>
</Expander>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Edit:
loc refers to the namespace in which the DepartmentParticipant and EmployeeParticipant are present. Hope you are familiar with adding namespaces.
Say for example I have the following type:
public class Site
{
public string Name { get; set; }
public int SiteId { get; set; }
public bool IsLocal { get; set; }
}
The above type can be assigned to be held in a Propety in a ViewModel like so assuming a corresponding backing field has been created but omitted here ofc:
public Site SelectedSite
{
get { return _selectedSite; }
set
{
_selectedSite = value;
// raise property changed etc
}
}
In my xaml a straight forward binding would be:
<TextBlock x:Name="StatusMessageTextBlock"
Width="Auto"
Height="Auto"
Style="{StaticResource StatusMessageboxTextStyle}"
Text="{Binding MessageToDisplay,
Mode=OneWay,
UpdateSourceTrigger=PropertyChanged}" />
Can you extend a binding by using the dot notation syntax? e.g:
<TextBlock x:Name="StatusMessageTextBlock"
Width="Auto"
Height="Auto"
Style="{StaticResource StatusMessageboxTextStyle}"
**Text="{Binding SelectedSite.Name,**
Mode=OneWay,
UpdateSourceTrigger=PropertyChanged}" />
Seems like a an interesting feature but my gut instinct is a no as my DC is being assigned at RunTime so at DesignTime or CompileTime, I can't see any clues that could make this feature work or not?
Correct me if have misunderstood what a complex object is, I have simplified mine down for the sake of this question.
Of course this is possible. However, WPF needs to know when any property along the path has changed. To that end, you need to implement INotifyPropertyChanged (or other supported mechanisms). In your example, both Site and the VM containing SelectedSite should implement change notification).
Here's how you could implement the functionality you specified in your question:
// simple DTO
public class Site
{
public string Name { get; set; }
public int SiteId { get; set; }
public bool IsLocal { get; set; }
}
// base class for view models
public abstract class ViewModel
{
// see http://kentb.blogspot.co.uk/2009/04/mvvm-infrastructure-viewmodel.html for an example
}
public class SiteViewModel : ViewModel
{
private readonly Site site;
public SiteViewModel(Site site)
{
this.site = site;
}
// this is what your view binds to
public string Name
{
get { return this.site.Name; }
set
{
if (this.site.Name != value)
{
this.site.Name = value;
this.OnPropertyChanged(() => this.Name);
}
}
}
// other properties
}
public class SitesViewModel : ViewModel
{
private readonly ICollection<SiteViewModel> sites;
private SiteViewModel selectedSite;
public SitesViewModel()
{
this.sites = ...;
}
public ICollection<SiteViewModel> Sites
{
get { return this.sites; }
}
public SiteViewModel SelectedSite
{
get { return this.selectedSite; }
set
{
if (this.selectedSite != value)
{
this.selectedSite = value;
this.OnPropertyChanged(() => this.SelectedSite);
}
}
}
}
And your view might look something like this (assuming a DataContext of type SitesViewModel):
<ListBox ItemsSource="{Binding Sites}" SelectedItem="{Binding SelectedSite}"/>
Below is what worked for me:
public Site SelectedSite
{
get { return _selectedSite; }
set
{
_selectedSite = value;
RaisePropertyChanged("SelectedSite");
}
}
In my xaml I was able to do:
<TextBox Name="tbSiteName"
Width="250"
Height="30"
Margin="0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
IsReadOnly="True"
Style="{StaticResource MainTextBoxStyle}"
Text="{Binding SelectedSite.Name,
Mode=OneWay,
UpdateSourceTrigger=PropertyChanged}" />
This allows you to access data members off the Site Type without having to create individual properties that wrap each data member on the Site Type. Then individual controls can bind to each property declared in the VM. In a one to one fashion, this aproach can become rather verbose. The binding extension attached to the Text property of the TextBox control shown above, shows that we are not binding to a simple straight forward property but actually to a custom type. Potentially removing the need to create more public properties.