WPF Binding to member of a class within a class - c#

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" />

Related

WPF, MVVM Populating Cascading ComboBoxes from a dictionary

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));
}
}

Filling a ListBox from two TextBoxes with DataBinding

I have a ListBox I want to fill with data from two TextBoxesby clicking a Button. I think the problem comes from the differents textblock i have in my listbox. Here is what i want in image :
TheUI
The MainWindow.xaml of my listbox :
<ListBox x:Name="listBox"
ItemsSource="{Binding Issues}" Grid.Column="1" HorizontalAlignment="Left" Height="366" VerticalAlignment="Top" Width="453" Margin="0,0,-1,0">
<StackPanel Margin="3">
<DockPanel >
<TextBlock FontWeight="Bold" Text="Issue:"
DockPanel.Dock="Left"
Margin="5,0,10,0"/>
<TextBlock Text=" " />
<TextBlock Text="{Binding Issue}" Foreground="Green" FontWeight="Bold" />
</DockPanel>
<DockPanel >
<TextBlock FontWeight="Bold" Text="Comment:" Foreground ="DarkOrange"
DockPanel.Dock="Left"
Margin="5,0,5,0"/>
<TextBlock Text="{Binding Comment}" />
</DockPanel>
</StackPanel>
</ListBox>
My MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public sealed class ViewModel
{
public ObservableCollection<Issue> Issues { get; private set; }
public ViewModel()
{
Issues = new ObservableCollection<Issue>();
}
}
private void addIssue_Click(object sender, RoutedEventArgs e)
{
var vm = new ViewModel();
vm.Issues.Add(new Issue { Name = "Jon Skeet", Comment = "lolilol" });
DataContext = vm;
InitializeComponent();
}
}
My Issue.cs :
public sealed class Issue
{
public string Name { get; set; }
public string Comment { get; set; }
}
I follow this tutorial but i don't want to implement a Database :
Tuto
I also try to use this stackoverflow question
The error i have is 'System.InvalidOperationException' The Items collection must be empty to use ItemsSource
But not sure this is the heart of the problem.
Remove whatever you have inserted between <ListBox> and </ListBox>, as it is treated as part of Items collection.
Instead shift that content between <ListBox.ItemTemplate>...</ListBox.ItemTemplate>.
You don't need to update Context and InitializeComponent every time, atleast to your case.
public partial class MainWindow : Window
{
ViewModel vm = new ViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = vm;
}
public sealed class ViewModel
{
public ObservableCollection<Issue> Issues { get; private set; }
public ViewModel()
{
Issues = new ObservableCollection<Issue>();
}
}
private void addIssue_Click(object sender, RoutedEventArgs e)
{
vm.Issues.Add(new Issue { Name = "Jon Skeet", Comment = "lolilol" });
}
}

hot to notify viewmodel property form model so that property value reflect in view

public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This the base class which implement INotifyPropertyChanged
public class LogActivity : ViewModelBase
{
private string messageLog;
public string MessageLog
{
get
{
return messageLog;
}
set
{
if (value != messageLog)
{
messageLog = value;
NotifyPropertyChanged("MessageLog");
}
}
}
}
This is my viewmodel class
public class SingleMessageViewModel : ViewModelBase
{
private LogActivity messagelog;
public SingleMessageViewModel()
{
messagelog = new LogActivity();
}
public LogActivity MessageLog
{
get
{
return messagelog;
}
set
{
if (value != messagelog)
{
messagelog = value;
NotifyPropertyChanged("MessageLog");
}
}
}
}
This is my view where the above property bind to:
<TextBox x:Name="TxtLog" Text="{Binding LogMessage, UpdateSourceTrigger=PropertyChanged}" Grid.Row="1" TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Visible" />
I want to use this property in one of my model class and send the notification to viewmodel about the property change so that It can bind to view given above.
Request you all to provide me some help on this. Thanks in advance.. :)
This Code:
<TextBox x:Name="TxtLog" Text="{Binding LogMessage, UpdateSourceTrigger=PropertyChanged}" Grid.Row="1" TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Visible" />
Needs to be:
<TextBox x:Name="TxtLog" Text="{Binding MessageLog.MessageLog, UpdateSourceTrigger=PropertyChanged}" Grid.Row="1" TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Visible" />
Note the Binding, you are binding to a property within LogActivity and it needs to be pointed to it properly.

Data Binding Issue

I am having a major problem binding my data from TextBox to ViewModel To TextBlock. I have set up my following Xaml Code like so:
<Page
x:Class="digiBottle.MainPage"
DataContext="{Binding Source=UserProfile}"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:digiBottle"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
DataContext="{Binding Source=UserProfile}">
<TextBlock HorizontalAlignment="Left" Margin="219,72,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Height="32" Width="232" Text="{Binding userFirstName, Mode=OneWay}"/>
<TextBox HorizontalAlignment="Left" Margin="39,72,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="111" Text="{Binding userFirstName, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
The .cs file I am trying to use as a source is defined as follows:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace digiBottle.Model
{
public class UserProfile : INotifyPropertyChanged
{
public int ID { get; set; }
public string userFirstName;
public string userLastName { get; set; }
public int age { get; set; }
public int weight { get; set; }
public int height { get; set; }
public DateTime signupTime { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public UserProfile()
{
userFirstName = "First Name";
}
private void RaisePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public UserProfile getCopy()
{
UserProfile copy = (UserProfile)this.MemberwiseClone();
return copy;
}
}
}
What am i doing wrong when trying to bind my TextBox and TextBlock to userFirstName in the UserProfile.cs Source. ANy help would be a major help!
Thank you
The first thing I notice here is that your properties (setter) are not raising event change. You need to call RaisePropertyChanged in your properties setter.
I would have written it like
A private field
private String _userFirstName;
Then in constructor
public UserProfile()
{
this._userFirstName = "First Name";
}
With Property raising event
public String UserFirstName
{
get { return this._userFirstName; }
set
{
this._userFirstName = value;
this.RaisePropertyChanged("UserFirstName");
}
}
And then in XAML, bind it with property "UserFirstName" with two way binding
<TextBlock HorizontalAlignment="Left" Margin="219,72,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Height="32" Width="232" Text="{Binding UserFirstName, Mode=OneWay}"/>
<TextBox HorizontalAlignment="Left" Margin="39,72,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="111" Text="{Binding UserFirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
DataBinding can be hard to understand at first. Please refer to Data binding for Windows Phone 8 to get yourself started.
For your code: Here are the fixes you will need:
Remember you can only bind to a property.
You need to raise the event on the set action.
You may need twoway binding on the textbox depending on the actions you want.
You need to set the DataContext for both textbox and the textblock.
Here are the changes:
CS
public class UserProfile : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string user_first_name;
public String UserFirstName
{
get { return user_first_name; }
set
{
user_first_name = value;
OnPropertyChanged("UserFirstName");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
UserProfile up = new UserProfile();
this.tb1.DataContext = up;
this.tb2.DataContext = up;
}
}
XAML
<TextBlock x:Name="tb2" TextWrapping="Wrap" Text="{Binding UserFirstName}"/>
<TextBox x:Name="tb1" HorizontalAlignment="Left" Height="72" Margin="14,475,0,0" Grid.Row="1" TextWrapping="Wrap" Text="{Binding UserFirstName, Mode=TwoWay}" VerticalAlignment="Top" Width="456" />

How to use the RelayCommand

I have application that works with Model View ViewModel.
In my model I have a List based on my Client class.
public class Client
{
public string Name { get; set; }
public string Ip { get; set; }
public string Mac { get; set; }
}
In my ClientRepository I make a List from XML file with my Client class.
public ClientRepository()
{
var xml = "Clients.xml";
if (File.Exists(xml))
{
_clients = new List<Client>();
XDocument document = XDocument.Load(xml);
foreach (XElement client in document.Root.Nodes())
{
string Name = client.Attribute("Name").Value;
string Ip = client.Element("IP").Value;
string Mac = client.Element("MAC").Value;
_clients.Add(new Client() { Mac = Mac, Name = Name, Ip = Ip });
}
}
}
In my UI/UX I have 3 Textboxes 1 for MAC, 1 IP and 1 Name I also have a Button thats has a binding to AddClientCommand.
<Label Grid.Row="0" Grid.Column="0" Content="Host Name:"/>
<TextBox Grid.Row="0" Grid.Column="1" x:Name="tbHostName" Height="20" Text="{Binding Path=newClient.Name, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="1" Grid.Column="0" Content="IP Address:"/>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="tbIP" Height="20" Text="{Binding Path=newClient.Ip, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="2" Grid.Column="0" Content="MAC Address"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="tbMAC" Height="20" Text="{Binding Path=newClient.Mac, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Row="3" Grid.Column="0" Content="Remove" x:Name="bRemove" Margin="3 0 3 0" Click="bRemove_Click"/>
<Button Grid.Row="3" Grid.Column="1" Content="Add" x:Name="bAdd" Margin="3 0 3 0" Click="bAdd_Click" Command="{Binding AddClientCommand}"/>
To come to my point: What I want to know is what is the best way to implement the AddClientCommand?
What I currently have and I know it doesn't work:
public ClientViewModel()
{
_repository = new ClientRepository();
_clients = _repository.GetClients();
WireCommands();
}
private void WireCommands()
{
AddClientCommand = new RelayCommand(AddClient);
}
public Client newClient
{
get
{
return _newClient;
}
set
{
_newClient = value;
OnPropertyChanged("newClient");
AddClientCommand.isEnabled = true;
}
}
public void AddClient()
{
_repository.AddClient(newClient);
}
RelayCommand class:
public class RelayCommand : ICommand
{
private readonly Action _handler;
private bool _isEnabled;
public RelayCommand(Action handler)
{
_handler = handler;
}
public bool isEnabled
{
get { return true; }
set
{
if (value != isEnabled)
{
_isEnabled = value;
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, EventArgs.Empty);
}
}
}
}
public bool CanExecute(object parameter)
{
return isEnabled;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_handler();
}
}
I recommend you to use DelegateCommands, you will find this class in many MVVM frameworks:
public ICommand AddClientCommand
{
get
{
return new DelegateCommand(AddClient, CanExecuteAddClient);
}
}
I also see that _clients is of type List<Client>. If you are binding this to the UI to see the clients list, changes will not be notified unless you use ObservableCollection<Client>
Edit: As someone pointed out in comments, you should create the _newClient. Be aware of creating a new one for each client added, or you will end up adding the same instance of Client over and over!
Have you just tried putting your command into a property something like this?:
public ICommand AddClientCommand
{
get { return new RelayCommand(AddClient, CanAddClient); }
}
public bool CanAddClient()
{
return newClient != null;
}
Put whatever logic you want to inside the CanAddClient to enable or disable the ICommand.
Ahhhh... I see... you have the wrong implementation of the RelayCommand. You need one that uses the CanExecuteChanged event handler... you can find the correct implementation in the RelayCommand.cs page on GitHub.

Categories