WPF, MVVM Populating Cascading ComboBoxes from a dictionary - c#

So, I have a UserControl which contains 2 comboboxes, which I want to be filled from a dictionary. So ComboBoxA gets filled with dictionary keys, and ComboBoxB get filled with the dictionary[ComboBoxA selected item]. How can I achieve that using MVVM? Category is basically int, and Parameter is a string.
My code so far:
Model
public class CategoryUserControlModel
{
public Dictionary<Category, List<Parameter>> parametersOfCategories { get; set;}
public Category chosenCategory { get; set; }
public Parameter chosenParameter { get; set; }
}
ViewModel
public class CategoryUserControlViewModel
{
public CategoryUserControlViewModel(CategoryUserControlModel controlModel)
{
Model = controlModel;
}
public CategoryUserControlModel Model { get; set; }
public Category ChosenCategory
{
get => Model.chosenCategory;
set
{
Model.chosenCategory = value;
}
}
public Parameter ChosenParameter
{
get => Model.chosenParameter;
set => Model.chosenParameter = value;
}
}
XAML
<Grid>
<ComboBox x:Name="Categories" HorizontalAlignment="Left" Margin="0,14,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Model.parametersOfCategories.Keys}"/>
<TextBlock x:Name="Text" HorizontalAlignment="Left" Height="15" Margin="0,-2,0,0" TextWrapping="Wrap" Text="Категория" VerticalAlignment="Top" Width="60"/>
<ComboBox x:Name="Parameter" HorizontalAlignment="Left" Margin="125,14,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Model.parametersOfCategories.Values}/>
<TextBlock x:Name="ParameterText" HorizontalAlignment="Left" Height="15" Margin="125,-2,0,0" TextWrapping="Wrap" Text="Параметр" VerticalAlignment="Top" Width="60"/>
</Grid>
</UserControl>

You are using inappropriate names for your model and viewmodel, they should never be related to a view, and your model should not have members with names that give the impression that the model need user interaction. Consider an improved version similar to this one:
public class CategoryWithParameterModel
{
public Dictionary<Category, List<Parameter>> ParametersOfCategories { get; set;}
public Category Category { get; set; }
public Parameter Parameter { get; set; }
}
Your viewmodel must implement INotifyPropertyChanged interface in order to inform the UI that it need to refresh bindings, this is not necessary for model since you are wrapping it in viewmodel. That is said, your viewmodel definition would become something like:
public class CategoryWithParameterViewModel : INotifyPropertyChanged
{ ... }
Next, since you want to bind to a list from the dictionary then your viewmodel have to expose a property which point to that list, let's call it AvailableParameters, so it should be defined like this:
public List<Parameter> AvailableParameters
{
get
{
if (Model.ParametersOfCategories.ContainsKey(ChosenCategory))
return Model.ParametersOfCategories[ChosenCategory];
else
return null;
}
}
This is the property that need to be bound to ItemsSource of second combobox named "Parameter" :)
However, the property ChosenCategory is not bound at all so you need to bind it to selected item of first combobox to be able to detect user choice which allow the viemodel to find the list of parameters, same thing applies to ChosenParameter, so here is the updated xaml code:
<Grid>
<ComboBox x:Name="Categories" HorizontalAlignment="Left" Margin="0,14,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Model.ParametersOfCategories.Keys}" SelectedItem="{Binding ChosenCategory}"/>
<TextBlock x:Name="Text" HorizontalAlignment="Left" Height="15" Margin="0,-2,0,0" TextWrapping="Wrap" Text="Категория" VerticalAlignment="Top" Width="60"/>
<ComboBox x:Name="Parameter" HorizontalAlignment="Left" Margin="125,14,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding AvailableParameters}" SelectedItem="{Binding ChosenParameter}"/>
<TextBlock x:Name="ParameterText" HorizontalAlignment="Left" Height="15" Margin="125,-2,0,0" TextWrapping="Wrap" Text="Параметр" VerticalAlignment="Top" Width="60"/>
</Grid>
Lastly, you have to notify UI when ChosenCategory has changed so for this you will need to raise PropertyChanged event for AvailableParameters. Implementing that will make the viewmodel become something like this:
public class CategoryWithParameterViewModel : INotifyPropertyChanged
{
public CategoryWithParameterViewModel(CategoryWithParameterModel model)
{
Model = model;
}
// This should be read-only
public CategoryWithParameterModel Model { get; /*set;*/ }
public Category ChosenCategory
{
get => Model.Category;
set
{
Model.Category = value;
OnPropertyChanged(nameof(AvailableParameters));
}
}
public Parameter ChosenParameter
{
get => Model.Parameter;
set => Model.Parameter = value;
}
public List<Parameter> AvailableParameters
{
get
{
if (Model.ParametersOfCategories.ContainsKey(ChosenCategory))
return Model.ParametersOfCategories[ChosenCategory];
else
return null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

Related

WPF .NET unable to add new instance to ObservableCollection

I am new to WPF and losing my mind with issues. I have a view, viewmodel and model. I want the user user to fill in some information in the view, press button to confirm and then have a new instance of the model (with the user specified parameters) added to the ObservableCollection and to my local database.
View: (unrelated stuff hidden)
<TextBox DataContext="{DynamicResource RiderequestViewModel}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="1" FontSize="12" Height="25" Text="{Binding Riderequest.Time}"/>
<TextBox DataContext="{DynamicResource RiderequestViewModel}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="2" FontSize="12" Height="25" Text="{Binding Riderequest.LocationFrom}"/>
<TextBox DataContext="{DynamicResource RiderequestViewModel}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="3" FontSize="12" Height="25" Text="{Binding Riderequest.LocationTo}"/>
<Button DataContext="{DynamicResource RiderequestViewModel}" x:Name="nextBtn" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="5" Content="Verder" Width="150" Foreground="White" Command="{Binding AddRiderequestCommand}" Click="NextBtn_Click"/>
ViewModel RiderequestViewModel:
namespace Drink_n_Drive.ViewModel
{
class RiderequestViewModel: BaseViewModel
{
private Riderequest riderequest;
private ObservableCollection<Riderequest> riderequests;
public ObservableCollection<Riderequest> Riderequests
{
get
{
return riderequests;
}
set
{
riderequests= value;
NotifyPropertyChanged();
}
}
public Riderequest Riderequest
{
get
{
return riderequest;
}
set
{
riderequest= value;
NotifyPropertyChanged();
}
}
public ICommand AddRiderequestCommand { get; set; }
public ICommand ChangeRiderequestCommand { get; set; }
public ICommand DeleteRiderequestCommand { get; set; }
public RiderequestViewModel()
{
LoadRiderequests(); //load existing from DB
LinkCommands(); //Link ICommands with BaseCommands
}
private void LoadRiderequests()
{
RiderequestDataService riderequestDS = new RiderequestDataService();
Riderequests= new ObservableCollection<Riderequests>(riderequestDS .GetRiderequests());
}
private void LinkCommands()
{
AddRiderequestCommand = new BaseCommand(Add);
ChangeRiderequestCommand = new BaseCommand(Update);
DeleteRiderequestCommand = new BaseCommand(Delete);
}
private void Add()
{
RiderequestDataService riderequestDS = new RitaanvraagDataService();
riderequestDS.InsertRiderequest(riderequest); //add single (new) instance to the DB
LoadRiderequests(); //Reload ObservableCollection from DB
}
private void Update()
{
if (SelectedItem != null)
{
RiderequestDataService riderequestDS = new RiderequestDataService();
riderequestDS.UpdateRiderequest(SelectedItem);
LoadRiderequests(); //refresh
}
}
private void Delete()
{
if (SelectedItem != null)
{
RiderequestDataService riderequestDS = new RiderequestDataService();
riderequestDS.DeleteRiderequest(SelectedItem);
LoadRiderequests();
}
}
private Riderequest selectedItem;
public Riderequest SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
NotifyPropertyChanged();
}
}
}
}
Pressing the button simply does nothing and I don't know why. I also have a diffrent page where I want to show a datagrid of all instances in the ObservableCollection like this:
<DataGrid Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="3" DataContext="{DynamicResource RitaanvragenViewModel}" ItemsSource="{Binding Ritaanvragen}" SelectedItem="{Binding SelectedItem}" />
But the grid just shows completly empty. I have added some dummydata to my DB but still doesn't work.
My appologies for the mix of English and Dutch in the code.
I'm not 100% sure about it but i would do something like this:
As for first step I would change the TextBox to look like this:
<TextBox DataContext="{DynamicResource Ritaanvraag}" Margin="15,0,15,0" Grid.Column="1" Grid.Row="1" FontSize="12" Height="25" Text="{Binding Path=Time, Mode=OneWayToSource}"/>
There's no need to pass your ViewModel to it as a DataSource because your View's first few meta-data related lines should already define what ViewModel does it belong to.
When you not specify the type of your binding, it will use a default binding type which depends on the current object. You're using a TextBox now so it will have a TwoWay binding by default.
If you only want to accept data from the user and you don't want to show the data if your model has any then you should use OneWayToSource. (Note: OneWay is a direction between source -> view.)
I would also remove the DataSource from your DataGrid because you already set it's ItemSource:
<DataGrid Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="3" ItemsSource="{Binding Ritaanvragen}" SelectedItem="{Binding SelectedItem}" />

Remove ListBoxItem from ViewModel

I develop CRUD app for WindowsPhone 8.1. I can add data to ObservableCollection collection and this data is displayed on ListBox. I use MVVM pattern.
Full repository https://github.com/OlegZarevych/CRUD_WP81
View :
<ListBox x:Name="Storage" ItemsSource="{Binding Path=Models, Mode=TwoWay}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="30" Width="450">
<TextBlock x:Name="nameblock" Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And ViewModel class
public class ViewModel
{
public string NewName { get; set; }
public string NewSurname { get; set; }
public int NewAge { get; set; }
public int i=0 ;
public ObservableCollection<DataStorageModel> Models { get; set; }
//Event Handlers
public ICommand CreateClickCommand { get; set; }
public ICommand UpdateClickCommand { get; set; }
public ICommand DeleteClickCommand { get; set; }
public ViewModel()
{
CreateClickCommand = new RelayCommand(arg => CreateClickMethod());
UpdateClickCommand = new RelayCommand(arg => UpdateClickMethod());
DeleteClickCommand = new RelayCommand(arg => DeleteClickMethod());
Models = new ObservableCollection<DataStorageModel>() {};
}
private void CreateClickMethod()
{
Models.Add(new DataStorageModel() { Name = NewName, Surname = NewSurname, Age = NewAge, Count=i++ });
}
private void UpdateClickMethod()
{}
private void DeleteClickMethod()
{}
}
I want to change data and delete it. As i good understand, I need select count from ListBoxItems and delete(update) this count in ObservableCollection.
How can I work with XAML code from ViewModel class ?
How can I initiliaze Storage in ViewModel ?
Or in MVVM is the better way to resolve this problem ?
When you want to delete a model from the ListBox you typically need some way to identify the selected ListBoxItems (or models) that you want to delete; for that, consider having an IsSelected property on your models and bind it to a CheckBox inside the ListBoxItem data template.
Now, when you click on delete, the delete command can then easily look into the Models list and see which items are selected for deletion. After it deletes the items, it can then enumerate over the collection and recalculate the count value for the remaining items and update the field in the view model.
So, you don't have to access the XAML to update the count of the models. If you make the count property mutable then you wouldn't have to reinitialize the storage after you delete items from the list.
I added code t the Model
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Also added checkbox with bindin to View.
<ListBox x:Name="Storage" Background="Gray" FontSize="14" ItemsSource="{Binding Path=Models, Mode=TwoWay}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="60" Width="400" >
<CheckBox x:Name="checkbox" IsChecked="{Binding Path=IsSelected, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock x:Name="nameblock" Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
But IsSelected var doesn't change when I check checkbox in item
Why ?

Is it possible to bind the property of a datacontext to the property of another datacontext

I have a Class which I use in a control for defining colors :
public class ColorModel : INotifyPropertyChanged {
public Color Color{ get { ... } set { ... } }
}
I also have a class which I wish to use for defining two-color LinearGradient Brushes :
public class GradientModel : INotifyPropertyChanged {
public Color First{ get { ... } set { ... } }
public Color Last{ get { ... } set { ... } }
}
Both of these classes serve as DataContexts for the controls that are responsible for defining their respective values.
I want to be able to use the ColorModel that I have defined to dictate the value of the First and Last colors of the GradientModel ( using two separate controls, each housing a ColorModel as a DataContext ).
I'm endeavoring to adhere to the MVVM as closely as possible.
How can I go about accomplishing this task?
Another way to accomplish this could be to define your GradientModel as follows:
public class GradientModel : INotifyPropertyChanged {
public ColorModel First{ get { ... } set { ... } }
public ColorModel Last{ get { ... } set { ... } }
}
That is, instead of defining the properties as Color, define them as ColorModel. Then in the constructor, subscribe to PropertyChanged event and update your members accordingly.
Note: You'll have to update your binding paths from First to First.Color and so on.
ColorModel that I have defined to dictate the value of the First and Last colors of the GradientModel
Within ColorModel instance, subscribe to the instance of GradientModel's INotifyPropertyChanged mechanism. Then within the subscription operation method just detect the property Color change event and when it occurs extract its value and update the properties of First and Last accordingly.
By rule, Binding will work only on DependencyProperty of DependencyObject.
Make ColorModel, and GradientModel DependencyObject.
Create a MainViewModel so that you can set up a Binding.
public class MainViewModel
{
public ColorModel CM1 { get; set; }
public ColorModel CM2 { get; set; }
public GradientModel GM { get; set; }
public MainViewModel()
{
CM1 = new ColorModel();
CM2 = new ColorModel();
GM = new GradientModel();
Binding b1 = new Binding("Color");
b1.Source = CM1;
b1.Mode = BindingMode.OneWay;
BindingOperations.SetBinding(GM, GradientModel.FirstProperty, b1);
Binding b2 = new Binding("Color");
b2.Source = CM2;
b2.Mode = BindingMode.OneWay;
BindingOperations.SetBinding(GM, GradientModel.LastProperty, b2);
}
}
<TextBox x:Name="Ctrl1" HorizontalAlignment="Left" Height="23" Margin="32,48,0,0" TextWrapping="Wrap" Text="{Binding CM1.Color}" VerticalAlignment="Top" Width="120"/>
<TextBox x:Name="Ctrl2" HorizontalAlignment="Left" Height="23" Margin="32,103,0,0" TextWrapping="Wrap" Text="{Binding CM2.Color}" VerticalAlignment="Top" Width="120"/>
XAML ( I have used string type instead of Color type in your VMs for demo. )
<TextBlock HorizontalAlignment="Left" Height="23" Margin="210,77,0,0" TextWrapping="Wrap" Text="{Binding GM.First}" VerticalAlignment="Top" Width="120" Background="#FFE8D7D7"/>
<TextBlock HorizontalAlignment="Left" Height="23" Margin="352,77,0,0" TextWrapping="Wrap" Text="{Binding GM.Last}" VerticalAlignment="Top" Width="120" Background="#FFECD7D7"/>

Caliburn Micro Datagrid Binding

I'm using Caliburn Micro framework in a WPF application and I need to bind a collection to ItemsSource of a DatGrid.
Please consider below code:
Class
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public ObservableCollection<Subject> Subjects;
}
public class Subject
{
public string Title{ get; set; }
}
View Model
public class PersonViewModel : Screen
{
private Person _person;
public Person Person
{
get { return _person; }
set
{
_person = value;
NotifyOfPropertyChange(() => Person);
NotifyOfPropertyChange(() => CanSave);
}
}
....
}
View
<UserControl x:Class="CalCompose.ViewModels.PersonView" ...ommited... >
<Grid Margin="0">
<TextBox x:Name="Person_Id" HorizontalAlignment="Left" Height="23" Margin="10,52,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
<TextBox x:Name="Person_Name" HorizontalAlignment="Left" Height="23" Margin="10,90,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
<DataGrid ItemsSource="{Binding Person_Subjects}" Margin="10,177,0,0"></DataGrid>
</Grid>
</UserControl>
Problem 1:
When I run the application the TextBoxes are getting right values, but the data grid haven't populated.
Here i'm using deep property binding technique using convention "ClassName_PropertyName".
Problem 2
When I change the value of 'Name' property the
NotifyOfPropertyChange(() => Person) is never called. I would like to call the guard method when the text changes in the Name field.
Can anyone suggest me a simple solution to overcome this issues?
Thanks in advance.
Implement PropertyChangedBase on the Person class, then for Name we can write
private string name;
public string Name
{
get { return name; }
set
{
if (name == value)
return;
name = value;
NotifyOfPropertyChange(() => Name);
}
}
For the binding to the DataGrid, dont use the "deep binding", merely use
<DataGrid ItemsSource="{Binding Person.Subjects}" ...
I hope this helps.

WPF Binding to member of a class within a class

My first try at C#, WPF, and MVVM. I've looked at several answers and tutorials and just cannot seem to get this right.
I have a View a Model and a ViewModel file (Actually more, but trying to simplify). In the view I want to bind a textbox to a view model member. I also want to bind a button click to a view model method.
The delegate command for Login() works fine, but I can't seem to update the ID property in Acc.ID.
What would I need to change to be able to do both?
I understand I will probably need to implement the PropertyChanged event in the ViewModel instead of the Model...I just don't understand how.
What I can do is set the DataContextto user.Acc in the code behind to directly update the model, but then I obviously cannot bind to the Login() method.
ViewModel.cs
public class LoginVM
{
private ServerInterface _serverInterface;
private ICommand _loginCommand;
private EmployeeAccount _acc;
public ICommand LoginCommand
{
get { return _loginCommand; }
}
public LoginVM()
{
Acc = new EmployeeAccount();
_serverInterface = new ServerInterface();
_loginCommand = new DelegateCommand<String>(Login);
}
public EmployeeAccount Acc { get; set; }
private void Login(object state)
{
this.Acc.ID = _serverInterface.Encrypt(this.Acc.ID);
}
}
View.xaml.cs
public partial class LoginView : Window
{
public LoginView()
{
InitializeComponent();
BindInXaml();
}
private void BindInXaml()
{
base.DataContext = new LoginVM();
}
}
Model.cs
public class EmployeeAccount : INotifyPropertyChanged
{
String _id;
public EmployeeAccount()
{
ID = "5000";
Name = "George Washington";
isAdmin = true;
Pswd = "TheyDont";
}
public String ID
{
get { return _id; }
set
{
_id = value;
this.OnPropertyChanged("ID");
}
}
public string Name { get; set; }
public Boolean isAdmin { get; set; }
public string Pswd { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(
this, new PropertyChangedEventArgs(propName));
}
}
.xaml Put in only what matters really
<TextBox x:Name="txtLogInName" Margin="60,43,42,129" TextWrapping="Wrap" Text="{Binding Path=ID, UpdateSourceTrigger=PropertyChanged}" Width="120" Grid.Column="1" Grid.Row="1"/>
<Button x:Name="btnLogIn" Content="Log on" Command="{Binding LoginCommand}" Margin="160,151,10,23" Grid.Column="1" Grid.Row="1" RenderTransformOrigin="1.667,0.545"/>
<TextBlock x:Name="txtBlockPassReset" TextAlignment="Center" Grid.Row="1" Grid.Column="1" RenderTransformOrigin="1.348,1.765" Margin="60,101,42,78">
<Hyperlink>Reset Password</Hyperlink>
</TextBlock>
<PasswordBox x:Name="pswdBoxLoginPass" Grid.Column="1" HorizontalAlignment="Left" Margin="60,72,0,0" Grid.Row="1" VerticalAlignment="Top" Width="120" Password="Password"/>
Try changing your xaml from
<TextBox x:Name="txtLogInName" Margin="60,43,42,129" TextWrapping="Wrap"
Text="{Binding Path=ID, UpdateSourceTrigger=PropertyChanged}"
Width="120" Grid.Column="1" Grid.Row="1"/>
to
<TextBox x:Name="txtLogInName" Margin="60,43,42,129" TextWrapping="Wrap"
Text="{Binding Path=Acc.ID, UpdateSourceTrigger=PropertyChanged}"
Width="120" Grid.Column="1" Grid.Row="1" />

Categories