I have a button on my page so that when I click it another small window pops up letting you enter a Country. This will then appear as a label on my main screen.
This is is the save button.
<Button x:Name="btnSave" Content="Save" HorizontalAlignment="Left" Margin="31,82,0,0" VerticalAlignment="Top" Width="75" Click="btnSave_Click"/>
Then I have the code behind.
ICommand _AddCountry;
ViewModel _viewmodel;
public NewCountry(ICommand AddCountry, ViewModel viewModel)
{
InitializeComponent();
_Addcountry = AddCountry;
_viewmodel = viewModel;
}
private void btnSave_Click(object sender, RoutedEventArgs e)
{
string countryName = txtCountry.Text;
_viewmodel.RenameCountry(countryName);
_Addcountry.Execute(null);
this.Close();
}
The label is binded to the Title so that the Country will appear as that label.
<Label Content="{Binding Country}" Width="500" HorizontalAlignment="Left" >
Now this is my problem, when I create that Country at runtime, it creates a new instance of the NewCountryClass which has a <String>Country Property inside BUT also has a ICollection<string> Property named Places.
public class NewCountryClass : INotifyPropertyChanged
private string _country;
public string Country
{
get { return _country; }
set
{
if (_country.Equals(value))
return;
_country = value;
RaisePropertyChanged(() => Country);
}
}
private ICollection<string> _places;
public ICollection<string> Places
{
get
{
if (_places == null)
_places = new ObservableCollection<string>();
return _places;
}
set
{
if (value == _places)
return;
_places = value;
RaisePropertyChanged(() => Places);
}
}
When I right click on that Countrys label on my main screen and click Add Place on the drop down, I want that place to get added to that Country Instance of that class, inside the ICollection<string>Places.
The problem comes when if I create 3 titles on the screen(so it creates 3 different instances of AddCountryClass) that when I click Add place I cant find the instance of the Country I clicked on.
Other useful code in my ViewModel:
public void AddCountryCollection()
{
NewCountryClass newCountryClass = new NewCountyClass(newCountry,"");
Collections.Add(newCountryClass);
}
This is where I create the instance of the Country, When I enter my Country
public ObservableCollection<NewCountryClass> Collections
{
get { return collections; }
set { collections = value; OnPropertyChanged(() => Collections); }
}
This is where the Country gets passed**
public void AddPlaces()
{
NewCountryClass newCountryClass;
string Item = SelectedCountry.Country;
Collections.Select(w => w.Country);
}
This was me trying to have a go but It didn't achieve anything
Summary
If I create 3 Countrys which use 3 different instances of class NewContryClass, When I want to add something into the ICollection<string>Places of one of the Countrys, how do I find it?
disclaim i know non view stuff belongs in to the VM but thats the way i do it and i'm fine with it
MainV
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!--Here is how you could select your Country-->
<Label Content="Country" Grid.Column="0" Grid.Row="0" />
<ComboBox Grid.Column="1" Grid.Row="0"
DisplayMemberPath="Country" Margin="0,0,30,5"
ItemsSource="{Binding CountryCollection}"
SelectedItem="{Binding SelectedCountryItem,UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="+" Grid.Column="1" Grid.Row="0" Height="24" HorizontalAlignment="Right" Width="24" Margin="0,0,0,5"
Command="{Binding NewCountryCommand}"/>
<!--Here is how you could show your selected Country-->
<Label Content="Selected" Grid.Column="0" Grid.Row="1" />
<Grid Grid.Column="1" Grid.Row="1" DataContext="{Binding SelectedCountryItem}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Content="{Binding Country}"
Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2"/>
</Grid>
</Grid>
MainVM
private RelayCommand _NewCountryCommand; // you can find the RelayCommand Class on the web
public ICommand NewCountryCommand
{
get { return _NewCountryCommand ?? (_NewCountryCommand= new RelayCommand(param => this.OnNewCountry())); }
}
public void OnNewCountry()
{
var window = new Window(); // or whatever View you use
var vm = new NewCountryClass();
window.DataContext= vm;
window.ShowDialog();
if(vm.Result)// Result will be set to true
// if the user Click the Save Button in your CountryV
{
// here will be your logic if the user want to save his stuff
// in my case
CountryCollection.Add(vm)
SelectedCountryItem = vm;
}
}
public ObservableCollection<NewCountryClass> CountryCollection
{
get { return collections; }
set { collections = value;
OnPropertyChanged(() => CountryCollection); }
}
public NewCountryClass SelectedCountryItem
{
get { return selectedCountryItem; //}
set { selectedCountryItem = value;
OnPropertyChanged(() => SelectedCountryItem); }
}
and thats it.
You can also take a look at this Tut which provided some Samplecode
Related
Okay so I've now got a more specific question.
I'm trying to figure out how I can change the value (bool) of a label when two Textboxes are no longer empty. I cannot seem to figure out how to get it to work even though it seems very straight forward.
Could someone point me in the right direction?
Please see below my code.
Model (Person.cs)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PracticeUI.Model
{
public class Person
{
private string _firstName;
private string _lastName;
public string FullName
{
get
{
return _firstName + " " + _lastName;
}
set { }
}
public string FirstName
{
get
{
return _firstName;
}
set
{
_firstName = value;
}
}
public string LastName
{
get
{
return _lastName;
}
set
{
_lastName = value;
}
}
}
}
ViewModel (PersonViewModel.cs)
using PracticeUI.Model;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace PracticeUI.ViewModel
{
public class PersonViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Person _newPerson = new Person();
private ICommand _addPerson;
public Person NewPerson
{
get
{
return _newPerson;
}
set
{
_newPerson = value;
OnPropertyChanged("NewPerson");
}
}
public PersonViewModel()
{
_PersonList.Add(new Person() { FirstName = "Tom", LastName = "Barratt" });
_PersonList.Add(new Person() { FirstName = "Harriet", LastName = "Hammond" });
}
private ObservableCollection<Person> _PersonList = new ObservableCollection<Person>();
public ObservableCollection<Person> PersonList
{
get
{
return _PersonList;
}
set
{
_PersonList = value;
OnPropertyChanged("PersonList");
OnPropertyChanged("AddPersonCanExecute");
}
}
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public ICommand AddPersonCommand
{
get
{
if (_addPerson == null)
{
_addPerson = new RelayCommand(p => this.AddPersonCanExecute, p => this.AddPerson());
}
return _addPerson;
}
}
public bool AddPersonCanExecute
{
get
{
return _newPerson.FirstName != string.Empty || _newPerson.LastName != string.Empty;
}
}
public void AddPerson()
{
_PersonList.Add(new Person() { FirstName = _newPerson.FirstName, LastName = _newPerson.LastName });
OnPropertyChanged("PersonList");
}
}
}
View (MainWindow.xaml)
<Window x:Class="PracticeUI.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ViewModel="clr-namespace:PracticeUI.ViewModel"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<ViewModel:PersonViewModel x:Key="ViewModel"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<ListBox ItemsSource="{Binding Source={StaticResource ViewModel}, Path=PersonList}" Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="3" Height="200" Margin="0 0 0 20">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding FirstName}"/>
<Label Content="{Binding LastName}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Label Content="First Name:" Grid.Row="2" Grid.Column="1"/>
<TextBox Text="{Binding Source={StaticResource ViewModel}, Path=NewPerson.FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Grid.Row="2" HorizontalAlignment="Left" Grid.Column="2" Height="40" Width="200" Margin="10 5"/>
<Label Content="First Name:" Grid.Row="3" Grid.Column="1"/>
<TextBox Text="{Binding Source={StaticResource ViewModel}, Path=NewPerson.LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Grid.Row="3" HorizontalAlignment="Left" Grid.Column="2" Height="40" Width="200" Margin="10 5"/>
<Button Command="{Binding Source={StaticResource ViewModel}, Path=AddPersonCommand}" Content="Add Person" Width="120" Height="30" Grid.Row="4" Grid.Column="2"/>
<Label Content="{Binding Source={StaticResource ViewModel}, Path=AddPersonCanExecute, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" Grid.Row="2" Grid.Column="4"/>
</Grid>
</Window>
First, rename PersonViewModel to MainViewModel. It's not a viewmodel that represents a person, it's your main viewmodel for the whole program. It has a whole collection of Person; how is it one person? It isn't. Naming your classes well makes it much easier to keep track of what's what. We'll be renaming Person to PersonViewModel because it needs to be a viewmodel also, and it does actually represent a person.
You want the UI to look at the value of AddPersonCanExecute whenever there's a change in the value of NewPerson.FirstName or NewPerson.LastName.
What can cause those values to change?
One way is that NewPerson can change. So:
public Person NewPerson
{
get
{
return _newPerson;
}
set
{
_newPerson = value;
OnPropertyChanged(nameof(AddPersonCanExecute));
OnPropertyChanged(nameof(NewPerson));
}
}
Another way is that the user can type a new value into the textboxes bound to the FirstName and LastName properties of NewPerson. Then you and the UI are out of luck, because Person isn't a viewmodel. It never raises any events when its properties change. So make it a viewmodel.
public class ViewModelBase : INotifyPropertyChanged
{
// Copy your INotifyPropertyChanged implementation here from your main viewmodel
// Make your main viewmodel inherit from ViewModelBase
}
// Formerly PersonViewModel
public class MainViewModel : ViewModelBase
{
// We need this to be the actual type because we'll need to be calling
// RaiseCanExecuteChanged() on it. Or whatever equivalent.
private RelayCommand _addPerson;
// All the stuff PersonViewModel had.
// Stuff
// Stuff
// Stuff
}
// Remember, your old PersonViewModel is now named MainViewModel. This is the class
// that you used to call Person.
public class PersonViewModel : ViewModelBase
{
public string FullName
{
get
{
return _firstName + " " + _lastName;
}
// No empty set, not ever. Somebody will try to set FullName and the compiler
// will let him think it worked. But nothing will change. That's a bug.
//set { }
}
public string FirstName
{
get
{
return _firstName;
}
set
{
_firstName = value;
// Do the same for LastName. Careful you don't pass nameof(FirstName)
// over there.
OnPropertyChanged(nameof(FirstName));
OnPropertyChanged(nameof(FullName));
}
}
Now the UI knows when those properties change, but the main viewmodel still doesn't. But now that we have notifications from Person, that's solvable. We have to rewrite NewPerson again:
private PersonViewModel _newPerson = null;
public PersonViewModel NewPerson
{
get { return _newPerson; }
set
{
if (value != _newPerson)
{
// Take the handler off the old NewPerson, if any.
if (_newPerson != null)
{
_newPerson.PropertyChanged -= NewPerson_PropertyChanged;
}
_newPerson = value;
if (_newPerson != null)
{
_newPerson.PropertyChanged += NewPerson_PropertyChanged;
}
OnPropertyChanged(nameof(NewPerson));
OnPropertyChanged(nameof(AddPersonCanExecute));
// I don't know what your RelayCommand class looks like, but it should
// provide some way to force it to raise its CanExecuteChanged event.
// That's what the Button is waiting for to enable or disable itself.
_addPerson.RaiseCanExecuteChanged()
}
}
}
private void NewPerson_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(Person.FirstName):
case nameof(Person.LastName):
OnPropertyChanged(nameof(AddPersonCanExecute));
AddPerson
break;
}
}
Another point: Don't make your viewmodel a resource. It's not breaking your code, but it serves no purpose and creates extra work for you.
<Window.DataContext>
<ViewModel:PersonViewModel />
</Window.DataContext>
<Window.Resources>
<!-- remove it from here -->
</Window.Resources>
Now for all controls belonging to the Window itself, all your bindings can look like this:
<TextBox
Text="{Binding NewPerson.FirstName, UpdateSourceTrigger=PropertyChanged}"
Grid.Row="2"
HorizontalAlignment="Left"
Grid.Column="2"
Height="40" Width="200" Margin="10 5"
/>
Get rid of Mode=TwoWay on TextBox.Text; that property will cause bindings on it to be TwoWay by default. Keep UpdateSourceTrigger=PropertyChanged only on TextBox.Text: That will cause the textbox to update the viewmodel on every keystroke, instead of the default behavior of updating the viewmodel property only when the textbox loses focus. You don't need UpdateSourceTrigger=PropertyChanged on the command binding or the Label.Content binding, because those properties cannot ever update the viewmodel property. They're OneWay by default, and by the nature of what they do.
I have a questionnaire application and I would like to display the objects (questions + answers in my case) one at a time. Meaning that I would like the user to be displayed the first question, answer it, then click a button and get the 2nd question (in the same window), click another button / or the same button and get the 3rd question and so on. At the moment, I am having difficulties trying to bind the 2nd question to my button by using only XAML. Is it possible to do this without using code behind? If so please do give me an idea. Thank you in anticipation.
This is my ViewModel:
namespace TestAppMVVM.ViewModel
{
public class TestViewModel
{
public ObservableCollection<Test> BeginnerTests
{
get;
set;
}
public Test CurrentQuestion
{
get;
set;
}
public Test NextQuestion
{
get;
set;
}
public Test CurrentAnswer
{
get;
set;
}
public Test NextAnswer
{
get;
set;
}
public void LoadBeginner()
{
ObservableCollection<Test> tests = new ObservableCollection<Test>();
tests.Add(new Test { Index = 1, Question = "1.What is the capital of England ?", FirstAnswer = "Paris", SecondAnswer = "London", ThirdAnswer = "Berlin" });
tests.Add(new Test { Index = 2, Question = "2.What is the capital of France ?", FirstAnswer = "Paris", SecondAnswer = "London", ThirdAnswer = "Berlin" });
tests.Add(new Test { Index = 3, Question = "3.What is the capital of Germany ?", FirstAnswer = "Paris", SecondAnswer = "London", ThirdAnswer = "Berlin" });
BeginnerTests = tests;
CurrentQuestion = BeginnerTests[0];
CurrentAnswer = BeginnerTests[0];
NextQuestion = BeginnerTests[1];
NextAnswer = BeginnerTests[1];
}
}
}
And here I paste the View:
<Grid Background="Yellow">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height ="20"/>
<RowDefinition Height ="100"/>
<RowDefinition Height ="auto"/>
<RowDefinition Height ="auto"/>
<RowDefinition Height ="30"/>
<RowDefinition Height ="*"/>
<RowDefinition Height ="20"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="1" Grid.Row="1" FontSize="25" Grid.ColumnSpan="3" Text="{Binding CurrentQuestion.Question}"/>
<StackPanel Orientation="Horizontal" Grid.Column="2" Grid.Row="3" Grid.ColumnSpan="5">
<RadioButton FontWeight="Bold" Content="{Binding CurrentAnswer.FirstAnswer}"/>
<RadioButton FontWeight="Bold" Margin="10 0 0 0" Content="{Binding CurrentAnswer.SecondAnswer}"/>
<RadioButton FontWeight="Bold" Margin="10 0 0 0" Content="{Binding CurrentAnswer.ThirdAnswer}"/>
</StackPanel>
<Button FontWeight="Bold" x:Name="nextButton" Content="Next Question" Grid.Column="2" Grid.Row="5"/>
If you want to avoid using the code behind and implement MVVM for this, you will need to implement ICommand.
Put this in your ViewModel:
public ICommand ExecuteCommand
{
get
{
if (_command == null)
{
_command = new RelayCommand(param => this.NextQuestion());
}
return _command;
}
}
public void NextQuestion()
{
//Do Stuff Here
}
Bind your button to it by adding this to your XAML:
Command="{Binding Path=ExecuteCommand}"
And, finally, add a RelayCommand class to your project:
public class RelayCommand : ICommand
{
readonly Action<object> _ActionToExecute;
readonly Predicate<object> _ActionCanExecute;
public RelayCommand(Action<object> inActionToExecute): this(inActionToExecute, null)
{
}
public RelayCommand(Action<object> inActionToExecute, Predicate<object> inActionCanExecute)
{
if (inActionToExecute == null)
throw new ArgumentNullException("execute");
_ActionToExecute = inActionToExecute;
_ActionCanExecute = inActionCanExecute;
}
public bool CanExecute(object parameter)
{
return _ActionCanExecute == null ? true : _ActionCanExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_ActionToExecute(parameter);
}
}
There's a way which is exposing an ICommand in ViewModel named MoveNext
The ICommand interface is here https://msdn.microsoft.com/en-us/library/system.windows.input.icommand(v=vs.110).aspx
class MoveNextCommand : ICommand
{
...... <Your implementation>
}
public class TestViewModel
{
...
private ICommand _moveNextCmd;
public ICommand MoveNextCmd
{
get { return _moveNextCmd ?? (_moveNextCmd = new MoveNextCommand()); }
}
...
}
In XAML
<Button FontWeight="Bold" x:Name="nextButton" Content="Next Question" Grid.Column="2" Grid.Row="5" Command={Binding MoveNextCmd} />
The command's logic is just changing the CurrentQuestion of the ViewModel.
The important thing here is that your ViewModel must implement INotifyPropertyChanged interface and setter of the properties must call OnPropertyChanged, otherwise you won't see any update when changing the property's value.
I've got a grouped Xamarin ListView, and found it troublesome updating the values within the ListView.
ViewModel;
public class MainViewModel
{
private ObservableCollection<ObservableGroupCollection<string, TaskItem>> _userTasksGrouped;
public ObservableCollection<ObservableGroupCollection<string, TaskItem>> UserTasksGrouped
{
get => _userTasksGrouped;
set
{
_userTasksGrouped = value;
OnPropertyChanged();
}
}
private void PopulateMyTasks()
{
UserTasks.Clear();
foreach (var task in MyTasks)
{
UserTasks.Add(new TaskItem(task));
}
DoTaskGrouping();
}
private void DoTaskGrouping()
{
UserTasksGrouped.Clear();
var groupedData =
UserTasks
.OrderBy(ut => ut.Name)
.GroupBy(ut => ut.StateEnum.ToString())
.Select(ut => new ObservableGroupCollection<string, TaskItem>(ut))
.ToList();
foreach (var group in groupedData)
UserTasksGrouped.Add(group);
}
public void UpdateTaskItem(TaskChanged taskChanged)
{
// update listing
var usertask = UserTasksGrouped.First(utg => utg.Any(ut => ut.Task.Id == taskChanged.Task.Id))
.First(t => t.Task.Id == taskChanged.Task.Id);
if (usertask != null)
{
usertask.Task = taskChanged.Task;
}
else
{
// something else
}
DoTaskGrouping();
}
public MainViewModel()
{
UserTasks = new ObservableCollection<TaskItem>();
UserTasksGrouped = new ObservableCollection<ObservableGroupCollection<string, TaskItem>>();
}
}
XAML;
<ListView
IsGroupingEnabled="true"
ItemsSource="{Binding UserTasksGrouped}"
GroupDisplayBinding="{Binding Key}"
GroupShortNameBinding="{Binding Key}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="50*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<BoxView
Grid.Row="0" Grid.RowSpan="2"
Grid.Column="0" Grid.ColumnSpan="2"
MinimumHeightRequest="16"
MinimumWidthRequest="16"
Margin="0"
Color="{Binding StatusColour}" />
<Label
Grid.Row="0" Grid.Column="2"
Text="{Binding Name}"
Style="{StaticResource LabelStyle}"
WidthRequest="200" />
</Grid>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Task Item:
public class TaskItem : INotifyPropertyChanged
{
private MyTask _task;
public MyTask Task
{
get => _task;
set
{
_task = value;
Name = value.Name;
OnPropertyChanged();
}
}
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public string StatusColour => "Green"
}
The problem when I call DoTaskGrouping() for the second time to force a refresh the App crashes with an UnhandledException. I managed to track this down to a "An item with the same key has already been added" error, so I added the ViewCell.View thinking it might fix things. Please help, I have a few questions;
How do I fix the error "An item with same key has already been added" and successfully refresh my grouped listing.
Do Observable collections within observable collections update themselves. Meaning if the properties or what their grouped by changes, so does the grouping? Meaning an item can move between groups?
Is my approach sound from a MVVM perspective? TaskItem could do with cleaning up.
Fixed by removing from the MainViewModel
public ObservableCollection<ObservableGroupCollection<string, TaskItem>> UserTasksGrouped
DoTaskGrouping() building the collection afresh each time;
UserTasksGrouped = new ObservableCollection<ObservableGroupCollection<string, TaskItem>>(groupedData);
I have an WPF application using MVVM.
I have a DataGrid bound to an ObservableCollection and a TextBox bound to the DataGrid SelectedItem, so when I click an item in the DataGrid, the TextBox is populated.
I also have a Button using Command and CommandParameter and using RelayCommand to check if the TextBox is empty and then disabling the Button.
That all works great, if I use UpdateSourceTrigger=PropertyChanged. The thing I don't like is because of the binding, if the user changes the text in the TextBox, the DataGrid record is edited. If the user then changes their mind about changing the record, and clicks somewhere else, the record in the DataGrid still shows the edited text.
What I have tried is using Mode=OneWay on the TextBox binding, which works in that it doesn't update the DataGrid record. After the data is saved to the database, I need to manually refresh the DataGrid to show the changes.
The code I have in my code behind is the DataGrid's SelectionChanged event which sets a property on the ViewModel to the selected item.
So in order to show the new changes, I thought adding a call to my GetCategories again after the changes would work. However when the code executes OnPropertyChanged("ReceivedCategories"), my CurrentCategory property becomes null.
My code:
CategoryModel.cs
public class CategoryModel
{
public int CategoryID { get; set; }
public string Description { get; set; }
readonly SalesLinkerDataContext _dbContext = new SalesLinkerDataContext();
public ObservableCollection<CategoryModel> GetCategories()
{
var result = _dbContext.tblSalesCategories.ToList();
List<CategoryModel> categoriesList = result.Select(item => new CategoryModel
{
CategoryID = item.CategoryID,
Description = item.Description.Trim()
}).ToList();
return new ObservableCollection<CategoryModel>(categoriesList);
}
internal bool UpdateCategory(int id, string description)
{
if (_dbContext.tblSalesCategories.Any(x => x.Description == description))
{
MessageBox.Show("A category with the same name already exists.");
return false;
}
try
{
var category = (from a in _dbContext.tblSalesCategories
where a.CategoryID == id
select a).FirstOrDefault();
if (category != null)
{
category.Description = description;
_dbContext.SubmitChanges();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
internal bool AddCategory(string description)
{
if (_dbContext.tblSalesCategories.Any(x => x.Description == description))
{
MessageBox.Show("A category with the same name already exists.");
return false;
}
var newCategory = new tblSalesCategory();
newCategory.Description = description;
try
{
_dbContext.tblSalesCategories.InsertOnSubmit(newCategory);
_dbContext.SubmitChanges();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
internal bool DeleteCategory(int id)
{
var result = _dbContext.tblSalesCategories.FirstOrDefault(x => x.CategoryID == id);
try
{
if (result != null)
{
_dbContext.tblSalesCategories.DeleteOnSubmit(result);
_dbContext.SubmitChanges();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
}
CategoriesViewModel.cs
public class CategoriesViewModel : ViewModelBase, IPageViewModel
{
public CategoryModel CurrentCategory = new CategoryModel();
public ObservableCollection<CategoryModel> Categories = new ObservableCollection<CategoryModel>();
public RelayCommand GetCategoriesRelay;
public RelayCommand UpdateCategoryRelay;
public RelayCommand AddCategoryRelay;
public RelayCommand DeleteCategoryRelay;
#region Get Categories Command
public ICommand GetCategoriesCommand
{
get
{
GetCategoriesRelay = new RelayCommand(p => GetCategories(),
p => CanGetCategories());
return GetCategoriesRelay;
}
}
private bool CanGetCategories()
{
return true;
}
private void GetCategories()
{
Categories = CurrentCategory.GetCategories();
ReceivedCategories = Categories;
}
#endregion
#region Update Category Command
public ICommand UpdateCategoryCommand
{
get
{
UpdateCategoryRelay = new RelayCommand(p => UpdateCategory((string) p),
p => CanUpdateCategory());
return UpdateCategoryRelay;
}
}
public bool CanUpdateCategory()
{
return !String.IsNullOrWhiteSpace(Description);
}
public void UpdateCategory(string description)
{
if (CurrentCategory.UpdateCategory(CurrentCategory.CategoryID, description))
{
GetCategories();
}
}
#endregion
#region Add Category Command
public ICommand AddCategoryCommand
{
get
{
AddCategoryRelay = new RelayCommand(p => AddCategory((string) p),
p => CanAddCategory());
return AddCategoryRelay;
}
}
private bool CanAddCategory()
{
return !String.IsNullOrWhiteSpace(Description);
}
private void AddCategory(string description)
{
if (CurrentCategory.AddCategory(description))
GetCategories();
}
#endregion
#region Delete Category Command
public ICommand DeleteCategoryCommand
{
get
{
DeleteCategoryRelay = new RelayCommand(p => DeleteCategory((int) p),
p => CanDeleteCategory());
return DeleteCategoryRelay;
}
}
private bool CanDeleteCategory()
{
return true;
}
private void DeleteCategory(int id)
{
if (CurrentCategory.DeleteCategory(id))
GetCategories();
}
#endregion
/// <summary>
/// Describes the name that will be used for the menu option
/// </summary>
public string Name
{
get { return "Manage Categories"; }
}
public string Description
{
get
{
return CurrentCategory.Description;
}
set
{
CurrentCategory.Description = value;
OnPropertyChanged("Description");
}
}
public ObservableCollection<CategoryModel> ReceivedCategories
{
get { return Categories; }
set
{
Categories = value;
OnPropertyChanged("ReceivedCategories");
}
}
}
CategoryView.xaml
<UserControl x:Class="SalesLinker.CategoriesView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="600" Background="White">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding GetCategoriesCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="45"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Margin="20,0,0,0" FontSize="20" HorizontalAlignment="Center" Content="Categories"/>
<DataGrid x:Name="LstCategories" Grid.Column="0" Grid.Row="1" AutoGenerateColumns="false"
ItemsSource="{Binding Path=ReceivedCategories, Mode=TwoWay}" SelectionChanged="Selector_OnSelectionChanged"
HorizontalScrollBarVisibility="Disabled" GridLinesVisibility="None"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserSortColumns="True" Background="White">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Description}" IsReadOnly="True" Header="Description" Width="300" />
</DataGrid.Columns>
</DataGrid>
<Button Command="{Binding AddCategoryCommand}" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" Height="50" Width="50" Margin="0,20,0,0" Background="Transparent" BorderThickness="0" BorderBrush="Transparent"
CommandParameter="{Binding ElementName=TbDescription, Path=Text}">
<Image Source="/Images/Plus.png"/>
</Button>
<Button Command="{Binding DeleteCategoryCommand}" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" Height="50" Width="50" Margin="0,75,0,0" Background="Transparent" BorderThickness="0" BorderBrush="Transparent"
CommandParameter="{Binding SelectedItem.CategoryID, ElementName=LstCategories, Mode=OneWay }">
<Image Source="/Images/Minus.png"/>
</Button>
<Grid Grid.Row="1" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="0" Grid.Column="0" Content="Description:"/>
<TextBox x:Name="TbDescription" DataContext="CategoryModel" Grid.Row="0"
Grid.Column="1" Width="250" Height="Auto" VerticalAlignment="Center"
HorizontalAlignment="Left" Margin="10,0,0,0"
Text="{Binding SelectedItem.Description, ElementName=LstCategories, Mode=OneWay}"/>
<Button Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" Margin="10,0,0,0"
Height="20" Width="120" Content="Update Description"
Command="{Binding UpdateCategoryCommand}"
CommandParameter="{Binding ElementName=TbDescription, Path=Text}" />
</Grid>
</Grid>
And I have also just noticed that using Mode=OneWay on the TextBox breaks my CanExecute pieces of code as well.
So all I can think of is either:
Find a way to bind another property to the TextBox as well?
Find a way of using UpdateSourceTrigger=PropertyChanged on the TextBox, but prevent the DataGrid being updated.
Any ideas?
For the null value in the Observable List you have to update the list with a copy of it and an Dispatcher.Invoke otherwise the gui will crash or show null I had a similar problem on update a observable list in a thread.
So you should write your changes in a list like:
//Your Copy List
ObservableCollection<CategoryModel> _ReceivedCategories;
//Your Execute command for the Gui
public void onUpdateExecuted(object parameter){
Dispatcher.Invoke(new Action(() => ReceivedCategories = new ObservableCollection <CategoryModel> (_ReceivedCategories));
}
//Your Undo Command if the Inputs are not ok
public void onUndoExecuted(object parameter){
Dispatcher.Invoke(new Action(() => _ReceivedCategories = new ObservableCollection <CategoryModel> (ReceivedCategories));
}
//Your Command on every input which is set
public void onInputExecuted(object parameter){
_ReceivedCategories.add(Your Object);
}
And you can add an update Command for the Main List so you first update the Copy List with the values and after an Command set the Copy Collection in the Main Collection
Hope that helps
I couldn't get the above suggestion by SeeuD1 to work, but have have figured out what the problem was.
I have The DataGrid's ItemsSource bound to ReceivedCategories and in the code behind, the SelectionChanged event to use to bind to the TextBox.
After the database save, I call my GetCategories() again to refresh the data.
So when ReceivedCategories gets the data again, the DataGrid's ItemsSource is updated, but not before the SelectionChange event fires trying to set my CurrentCategory to the SelectedItem. However there is no longer anything selected in the DataGrid (index is -1), so assignment fails and sets CurrentCategory to null.
Fix is to simply only assign CurrentCategory to the SelectedItem if something is selected.
Old code:
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (CategoriesViewModel)DataContext;
viewmodel.CurrentCategory = LstCategories.SelectedItems.Cast<CategoryModel>().FirstOrDefault();
}
Fixed:
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (CategoriesViewModel)DataContext;
if (LstCategories.SelectedIndex > -1)
viewmodel.CurrentCategory = LstCategories.SelectedItems.Cast<CategoryModel>().FirstOrDefault();
}
Thanks for the suggestions.
I'm trying to learn Prism MVVM, and i'm making a window with 2 fields and a button, that gets enabled when this two fields aren't empty.
The problem is that i can't find a way to make the method ObservesProperty() work on an object (Pessoa in that case). The CanExecuteAtualizar() method only gets called at the app startup, and when i edit the textfields Nome or Sobrenome nothing happens to the button and the method isn't fired...
I tried to work without a model, putting the Nome, Sobrenome and UltimaAtualizacao properties directly in the ViewModel and it works fine, disabling the button according to the return of the method CanExecuteAtualizar, but i wanted to use it with a model instead. Is there a way to do this?
ViewAViewModel.cs
public class ViewAViewModel : BindableBase
{
private Pessoa _pessoa;
public Pessoa Pessoa
{
get { return _pessoa; }
set { SetProperty(ref _pessoa, value); }
}
public ICommand CommandAtualizar { get; set; }
public ViewAViewModel()
{
Pessoa = new Pessoa();
Pessoa.Nome = "Gabriel";
CommandAtualizar = new DelegateCommand(ExecuteAtualizar, CanExecuteAtualizar).ObservesProperty(() => Pessoa.Nome).ObservesProperty(() => Pessoa.Sobrenome);
}
public bool CanExecuteAtualizar()
{
return !string.IsNullOrWhiteSpace(Pessoa.Nome) && !string.IsNullOrWhiteSpace(Pessoa.Sobrenome);
}
public void ExecuteAtualizar()
{
Pessoa.UltimaAtualizacao = DateTime.Now;
}
}
Pessoa.cs
public class Pessoa : BindableBase
{
private string _nome;
public string Nome
{
get { return _nome; }
set { SetProperty(ref _nome, value); }
}
private string _sobrenome;
public string Sobrenome
{
get { return _sobrenome; }
set { SetProperty(ref _sobrenome, value); }
}
private DateTime? _ultimaAtualizacao;
public DateTime? UltimaAtualizacao
{
get { return _ultimaAtualizacao; }
set { SetProperty(ref _ultimaAtualizacao, value); }
}
}
ViewA.xaml
<UserControl x:Class="PrismDemo.Views.ViewA"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PrismDemo.Views"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="500">
<Grid Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Content="Nome:" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="0" Margin="3" TabIndex="0" Text="{Binding Pessoa.Nome, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="Sobrenome:" Grid.Column="0" Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" Margin="3" TabIndex="1" Text="{Binding Pessoa.Sobrenome, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="Última atualização:" Grid.Column="0" Grid.Row="2" HorizontalAlignment="Left" VerticalAlignment="Center" />
<Label Grid.Column="1" Grid.Row="2" Margin="3" HorizontalAlignment="Left" Content="{Binding Pessoa.UltimaAtualizacao, Mode=TwoWay}" />
<Button Content="Atualizar" Grid.Column="1" Grid.Row="3" Width="70" Margin="2,2,3,2" HorizontalAlignment="Right" Command="{Binding CommandAtualizar}" />
</Grid>
</UserControl>
DelegateCommand.ObservesPropery doesn't support complex object properties. It only supports properties that exist on the ViewModel in the command is defined. This is because the lifecycle of complex objects are unknown, and a memory leak would be created if many instances of the object was created. My recommendation would be to define you property like this:
private Pessoa _pessoa;
public Pessoa Pessoa
{
get { return _pessoa; }
set
{
if (_pessoa != null)
_pessoa.PropertyChanged -= PropertyChanged;
SetProperty(ref _pessoa, value);
if (_pessoa != null)
_pessoa.PropertyChanged += PropertyChanged;
}
}
Then in the PropertyChanged method, call DelegateCommand.RaiseCanExecuteChanged
EDIT:
Complex property support is now available in Prism for Xamarin.Forms 7.0.
It is true that DelegateCommand.ObservesPropery does not support complex objects, but its the way Commands are meant to be used with Prism. Manually calling PropertyChanged is an ugly hack in my opinion and should be avoided. Also it bloates the code again which Prism tries to reduce.
Moving all properties of the complex type into the ViewModel on the other hand would reduce the readability of the ViewModel. The very reason you create complex types in such scenarios is to avoid having too many single properties there in the first place.
But instead you could move the Command definition inside the complex type. Then you can set ObservesProperty for all simple properties in the constructor of the complex type and everything works as expected.
Model:
using Prism.Commands;
using Prism.Mvvm;
using static System.String;
public class LoginData : BindableBase
{
public LoginData()
{
DbAddr = DbName = DbUser = DbPw = "";
TestDbCommand = new DelegateCommand(TestDbConnection, CanTestDbConnection)
.ObservesProperty(() => DbAddr)
.ObservesProperty(() => DbName)
.ObservesProperty(() => DbUser)
.ObservesProperty(() => DbPw);
}
public DelegateCommand TestDbCommand { get; set; }
public bool CanTestDbConnection()
{
return !IsNullOrWhiteSpace(DbAddr)
&& !IsNullOrWhiteSpace(DbName)
&& !IsNullOrWhiteSpace(DbUser)
&& !IsNullOrWhiteSpace(DbPw);
}
public void TestDbConnection()
{
var t = new Thread(delegate () {
Status = DatabaseFunctions.TestDbConnection(this);
});
t.Start();
}
private string _dbAddr;
public string DbAddr
{
get => _dbAddr;
set => SetProperty(ref _dbAddr, value);
}
...
}
ViewModel:
public class DatabaseConfigurationViewModel
{
public DatabaseConfigurationViewModel()
{
CurrentLoginData = new LoginData(true);
}
public LoginData CurrentLoginData { get; set; }
}
View:
<UserControl x:Class="TestApp.Views.DatabaseConfiguration"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<StackPanel Orientation="Vertical">
<Label>IP Adresse oder URL:</Label>
<TextBox Text="{Binding CurrentLoginData.DbAddr, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></TextBox>
...
<Button Command="{Binding CurrentLoginData.TestDbCommand}">Teste Verbindung</Button>
</StackPanel>
</Grid>
In the Prism version that I have (7.0.0.362), you can use ObserveCanExecute, and pass property HasChanges that is updated on every property change of your entity.
TestDbCommand = new DelegateCommand(TestDbConnection).ObservesCanExecute(() => HasChanged);
Pessoa = new Pessoa();
Pessoa.PropertyChanged += Pessoa_PropertyChanged;
Then update HasChanges with a validation method in the constructor, and detach the method in the destructor.
private void Pessoa_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
HasChanged = ValidatePessoa(Pessoa);
}
~YourViewModel()
{
Pessoa.PropertyChanged -= Pessoa_PropertyChanged;
}
bool _hasChanged;
public bool HasChanged { get => _hasChanged; set => SetProperty(ref _hasChanged, value); }