Trying to learn WPF and I have read/tested with a tutorial.
This is my scenario:
A wpf C# application.
My main window has a UserControl on it.
This UserControl has 4 buttons on it.
My intent is to bind each command(click) event to each button.
But instead of binding each button to its own class I want to bind each command event of these 4 buttons to 1 class.
So.. I wanted to pass a parameter to the CanExecute and Execute methods and I was/am trying to pass an enum to these methods.
so.. what i have got so far is this:
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
var commandChosen= parameter as TopMenuCommands;
return true;
}
public void Execute(object parameter)
{
var buttonChosen = parameter as MenuCommandObject;
evMenuChange(buttonChosen);
}
public enum enTopMenuCommands
{
Button1 = 0,
Button1 = 1,
Button1 = 2,
Button1 = 3
}
But how can I tie this to my main window?
I admit I maybe doing this all completely wrong but I am still learning.
thanks
My ICommand implementation takes an Action<object> in the constructor. The Execute method just invoked the Action.
That way the logic for each command is passed in from where it is created.
ICommand Implementation:
public class SimpleCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private Action<object> _execute;
private Func<bool> _canExecute;
public SimpleCommand(Action<object> execute) : this(execute, null) { }
public SimpleCommand(Action<object> execute, Func<bool> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object param)
{
if (_canExecute != null)
{
return _canExecute.Invoke();
}
else
{
return true;
}
}
public void Execute(object param)
{
_execute.Invoke(param);
}
protected void OnCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this,EventArgs.Empty);
}
#region Common Commands
private static SimpleCommand _notImplementedCommand;
public static ICommand NotImplementedCommand
{
get
{
if (_notImplementedCommand == null)
{
_notImplementedCommand = new SimpleCommand(o => { throw new NotImplementedException(); });
}
return _notImplementedCommand;
}
}
#endregion
}
Usage Example:
using System;
using System.Data.Entity;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using SqlMetaQuery.Model;
using SqlMetaQuery.ViewModels.ScriptList;
using SqlMetaQuery.Windows.EditQuery;
using WpfLib;
namespace SqlMetaQuery.Windows.Main
{
class MainWindowVm : WpfLib.ViewModel
{
public MainWindowVm()
{
if (!IsInDesignMode)
{
using (Context db = new Context())
{
ScriptTree = new ScriptTreeVm(db.Tags
.Include(t => t.Scripts)
.OrderBy(t => t.Name));
CurrentUser = db.Users.Where(u => u.UserName == "Admin").AsNoTracking().FirstOrDefault();
MiscTag = db.Tags.Where(t => t.Name == "Misc").AsNoTracking().FirstOrDefault();
}
}
}
public ScriptTreeVm ScriptTree { get; }
public Model.User CurrentUser { get; }
private Model.Tag MiscTag { get; }
private void SaveScript(Model.Script script)
{
using (var context = new Model.Context())
{
context.Scripts.Add(script);
context.SaveChanges();
}
}
#region Commands
private ICommand _exitCommand;
public ICommand ExitCommand
{
get
{
if (_exitCommand == null)
{
_exitCommand = new SimpleCommand((arg) => WindowManager.CloseAll());
}
return _exitCommand;
}
}
private ICommand _newScriptCommand;
public ICommand NewScriptCommand
{
get
{
if (_newScriptCommand == null)
{
_newScriptCommand = new SimpleCommand((arg) =>
{
var script = new Model.Script()
{
Title = "New Script",
Description = "A new script.",
Body = ""
};
script.Tags.Add(MiscTag);
var vm = new EditQueryWindowVm(script);
var result = WindowManager.DisplayDialogFor(vm);
// if (result.HasValue && result.Value)
//{
script.VersionCode = Guid.NewGuid();
script.CreatedBy = CurrentUser;
script.CreatedDate = DateTime.Now.ToUniversalTime();
SaveScript(script);
//}
});
}
return _newScriptCommand;
}
}
#endregion
}
}
XAML:
<Window x:Class="SqlMetaQuery.Windows.Main.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:SqlMetaQuery.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SqlMetaQuery.Windows.Main"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="600"
d:DataContext="{d:DesignInstance Type=local:MainWindowVm}"
mc:Ignorable="d">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Command="{Binding Path=NewScriptCommand}" Header="New Script..." />
<Separator />
<MenuItem Command="{Binding Path=ExitCommand}" Header="Exit" />
</MenuItem>
</Menu>
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:ScriptTree Grid.Row="0"
Grid.Column="0"
DataContext="{Binding Path=ScriptTree}" />
<GridSplitter Grid.Row="0"
Grid.Column="1"
Width="8"
VerticalAlignment="Stretch"
ResizeBehavior="PreviousAndNext" />
<Grid Grid.Row="0" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Grid.Row="0" Style="{StaticResource BorderStandard}">
<TextBlock Text="{Binding Path=ScriptTree.CurrentScript.Title}" />
</Border>
<Border Grid.Row="3" Style="{StaticResource BorderStandard}">
<TextBlock Text="{Binding Path=ScriptTree.CurrentScript.Body}" />
</Border>
</Grid>
</Grid>
</DockPanel>
</Window>
Related
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'm trying to bind a textbox (named Labour) and textblock (which shows time) with listview. Listview should show a names of labours and times that they're takes after cliking button "Add item". I've already tried to use observablecollection but I'm doing something wrong. Data in ListView should be separated in two columns.
How can i do that ?
XAML:
<Window x:Class="LabourTimer.View.LabourMainWindow"
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:local="clr-namespace:LabourTimer.View"
mc:Ignorable="d"
xmlns:viewmodel="clr-namespace:LabourTimer.ViewModel"
Title="Labour timer" Height="600" Width="515">
<Window.Resources>
<viewmodel:LabourViewModel x:Key="viewModel"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel>
<Label Content="Timer:" Grid.Row="0"/>
<TextBlock Text="{Binding CurrentTime}" Grid.Row="0"/>
<Label Content="Labour name:"/>
<TextBox x:Name="Labour" Text="{Binding labourName, Mode=TwoWay}"/>
</StackPanel>
<StackPanel Grid.Column="1">
<Button Content="Start/Stop" Command="{Binding StartStopCommand}"/>
<Button Content="Reset" Command="{Binding ResetCommand}"/>
<Button Content="Add to list" Command="{Binding AddCommand}"/>
<Button Content="Remove from list"/>
</StackPanel>
<ListView x:Name="listView" Height="Auto" Margin="0,5" Grid.Row="1" VerticalAlignment="Top" Grid.ColumnSpan="2" MinWidth="300">
<ListView.View>
<GridView>
<GridViewColumn Header="Labour name" Width="400" />
<GridViewColumn Header="Time in seconds" Width="100"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
ViewModel:
namespace LabourTimer.ViewModel
{
public class LabourViewModel : INotifyPropertyChanged
{
private DispatcherTimer _timer;
private string _currentTime;
private int _seconds;
private bool _running;
public ICommand StartStopCommand { get; set; }
public ICommand ResetCommand { get; set; }
public ICommand AddCommand { get; set; }
public string CurrentTime
{
get { return _currentTime; }
set { _currentTime = value;
OnPropetyChanged("CurrentTime");
}
}
public LabourViewModel()
{
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += new EventHandler(TimerTick);
_running = false;
CurrentTime = "0";
LoadCommands();
}
private void LoadCommands()
{
StartStopCommand = new CustomCommand(StartStopTimer, CanStartStopTimer);
ResetCommand = new CustomCommand(ResetTimer, CanResetTimer);
AddCommand = new CustomCommand(AddLabour, CanAddLabour);
}
private void AddLabour(object obj)
{
}
private bool CanAddLabour(object obj)
{
return true;
}
private bool CanResetTimer(object obj)
{
if (CurrentTime != "0")
return true;
else
return false;
}
private void ResetTimer(object obj)
{
_timer.Stop();
CurrentTime = "0";
_seconds = 0;
}
private void StartStopTimer(object obj)
{
if (_running == false)
{
_timer.Start();
_running = true;
}
else
{
_timer.Stop();
_running = false;
}
}
private bool CanStartStopTimer(object obj)
{
return true;
}
private void TimerTick(object send, EventArgs e)
{
_seconds++;
CurrentTime = _seconds.ToString();
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropetyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
CustomCommand:
public class CustomCommand : ICommand
{
private Action<object> execute;
private Predicate<object> canExecute;
public CustomCommand(Action<object> execute, Predicate<object> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public bool CanExecute(object parameter)
{
bool b = canExecute == null ? true : canExecute(parameter);
return b;
}
public void Execute(object parameter)
{
execute(parameter);
}
}
}
MainWindow:
public partial class LabourMainWindow : Window
{
public LabourMainWindow()
{
InitializeComponent();
DataContext = new LabourViewModel();
}
Please allow me to present a simplified version of my problem:
Lets say I have a main window called MainWindow in which I would like to display some Person objects in MainWindow. Now, in order to instantiate these Person objects I need a bunch of different fields such as name, age, profession, favourite food, etc...
Here is my solution:
I try to get all input fields and instantiate a Person in a secondary window and then send back the result to the main form.
MainWindow has a public method as follows:
public void (Person input)
{
// use the fields in input to add details to window
}
I have another window in the project called PersonInput that takes in its constructor a reference to a MainWindow and saves it in a field.
private MainWindow owner;
public PersonInput(MainWindow parent)
{
InitializeComponent();
owner = parent;
}
PersonInput has a number of input fields corresponding to the required fields of a Person object.
in addition it has a button called "AddPerson" with an associated onClick event handler as follows: (pseudoCode)
private void button_Click(object sender, RoutedEventArgs e)
{
//get all fields from this form..
String enteredName = this.nameText.Text;
//get more fields....
Person p = new Person(...);
//owner is MainWindow, send Back the Person so details can be displayed
owner.addPerson(p);
this.Close();
}
as you would expect, MainWindow has a Button named "AddPersonButton" which has an on click event handler like this:
private void button_Click(object sender, RoutedEventArgs e)
{
PersonInput x = new PersonInput(this); //pass this as a reference
//so this window can send us back the result when they have it
x.Show(); //open the child window so user can enter information
}
While this method works, I am quite convinced it is not the best practice way to do it. I would like to learn the idiomatic .net WPF way of doing this. Please enlighten me
PersonInput.xaml.cs
public class PersonInput : Window
{
public void PersonInput()
{
InitializeComponent();
Owner = Application.Current.MainWindow;
}
public static Person ShowDialog(Person initializer)
{
var vm = new PersonViewModel(initializer);
var dlg = new PersonInput() { DataContext = vm };
if (dlg.ShowDialog().GetValueOrDefault(false))
{
return vm.ToPerson();
}
return null;
}
private void OK_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
}
PersonInputViewModel.cs
public class PersonViewModel : ViewModelBase
{
public PersonViewModel(Person person = null)
{
if (person != null)
{
// Assuming Person has FirstName and LastName properties
FirstName = person.FirstName;
LastName = person.LastName;
// etc. etc. for all the rest
}
}
public Person ToPerson()
{
return new Person()
{
FirstName = this.FirstName,
LastName = this.LastName,
// etc. etc. for all other properties
};
}
private string _firstName = null;
public string FirstName {
get { return _firstName; }
set {
if (value != _firstName) {
_firstName = value;
OnPropertyChanged(nameof(FirstName));
}
}
}
}
ViewModelBase.cs
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
PersonInput.xaml
<Window xmlns:blahblahblah="Blah blah blah" etc etc
Title="Person"
Height="640"
Width="480"
ShowInTaskbar="False"
ResizeMode="CanResizeWithGrip"
WindowStartupLocation="CenterOwner"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="180" Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0">First Name</Label>
<TextBox
Grid.Row="0"
Grid.Column="1"
Text="{Binding FirstName}"
/>
<Label Grid.Row="1" Grid.Column="0">Last Name</Label>
<TextBox
Grid.Row="1"
Grid.Column="1"
Text="{Binding LastName}"
/>
<StackPanel
Orientation="Horizontal"
Grid.Column="1"
Grid.Row="10"
HorizontalAlignment="Right"
>
<Button Content="_OK" Click="OK_Click" />
<Button Content="_Cancel" Click="Cancel_Click" />
</StackPanel>
</Grid>
</Window>
The biggest weakness of this approach is the strong coupling between MainWindow and PersonInput.
A slightly better approach would be to use Observer Pattern and have the Main Window anonymously subscribe.
Simple solution example code:
public interface IAddPersonObserver
{
void OnPersonAdded(Person person);
}
public interface IAddPersonObservable
{
void Subscribe(IAddPersonObserver observer);
void Unsubscribe(IAddPersonObserver observer);
}
public class MainWindow : IAddPersonObserver
{
...
private void button_Click(object sender, RoutedEventArgs e)
{
PersonInput x = new PersonInput();
x.Subscribe(this);
x.Show();
}
public void OnPersonAdded(Person addedPerson)
{
addPerson(addedPerson); // or whatever view update code you want
}
}
Further improvements would revolve around separating the MainWindow from knowing what view gets created or shown, and having an intermediary object (such as a PersonRepository) store/hold/provide the important business data. This is much better than having the application data actually live inside the Views and Application Windows.
Another use MVVM practice:
Solution structure
public class Person {
public int Id { get; set; }
public string Name { get; set; }
public DateTime Birthday { get; set; }
}
public class PersonListViewModel : DependencyObject {
public ObservableCollection<Person> Items { get; set; }
public Person CurrentPerson
{
get { return (Person)GetValue(CurrentPersonProperty); }
set { SetValue(CurrentPersonProperty, value); }
}
public static readonly DependencyProperty CurrentPersonProperty = DependencyProperty.Register("CurrentPerson", typeof(Person), typeof(PersonListViewModel));
public ICommand AddCommand { get; set; }
public ICommand EditCommand { get; set; }
public PersonListViewModel() {
Items = new ObservableCollection<Person>();
AddCommand = new RelayCommand(p=> add() );
EditCommand = new RelayCommand(p=> { return CurrentPerson != null; }, p => edit());
}
private void add() {
Person p= new Person();
p.Id = Items.Count();
p.Name = "New Name";
p.Birthday = DateTime.Now;
Items.Add(p);
}
private void edit() {
var viewModel = new PersonItemViewModel(CurrentPerson);
var view = new View.PersonEditWindow();
view.DataContext = viewModel;
view.Show();
}
}
public class PersonItemViewModel : DependencyObject {
Person person;
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof(string), typeof(PersonItemViewModel) );
public DateTime Birthday
{
get { return (DateTime)GetValue(BirthdayProperty); }
set { SetValue(BirthdayProperty, value); }
}
public static readonly DependencyProperty BirthdayProperty = DependencyProperty.Register("Birthday", typeof(DateTime), typeof(PersonItemViewModel));
public PersonItemViewModel() {
}
public PersonItemViewModel(Person source) {
this.person = source;
Name = person.Name;
Birthday = person.Birthday;
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) {
base.OnPropertyChanged(e);
if (e.Property == NameProperty) {
person.Name = (string) e.NewValue;
}
if (e.Property == BirthdayProperty) {
person.Birthday = (DateTime)e.NewValue;
}
}
}
List Form:
<Window x:Class="WpfApplication1.View.PersonListWindow"
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:local="clr-namespace:WpfApplication1.View"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
Title="PersonListWindow" Height="300" Width="300"
xmlns:viewModel="clr-namespace:WpfApplication1.ViewModel"
d:DataContext="{d:DesignInstance Type=viewModel:PersonListViewModel, IsDesignTimeCreatable=True}"
>
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<DataGrid ItemsSource="{Binding Items}" SelectedItem="{Binding CurrentPerson, Mode=TwoWay}">
</DataGrid>
<StackPanel Grid.Row="1" Height="32" Orientation="Horizontal" HorizontalAlignment="Center">
<Button Margin="4" Command="{Binding AddCommand}">Add</Button>
<Button Margin="4" Command="{Binding EditCommand}">Edit</Button>
</StackPanel>
</Grid>
Edit Form:
<Window x:Class="WpfApplication1.View.PersonEditWindow"
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:local="clr-namespace:WpfApplication1.View"
mc:Ignorable="d"
Title="PersonEditWindow" Height="300" Width="300"
xmlns:viewModel="clr-namespace:WpfApplication1.ViewModel"
d:DataContext="{d:DesignInstance Type=viewModel:PersonItemViewModel, IsDesignTimeCreatable=True}"
>
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0">Name</TextBlock>
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Name}"></TextBox>
<TextBlock Grid.Row="1">Birthday</TextBlock>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Birthday}"></TextBox>
</Grid>
Results (Image)
I've spent the last days reading and trying to apply the Navigation pattern from this page: https://rachel53461.wordpress.com/2011/12/18/navigation-with-mvvm-2/
Now, after I got my project to work I'm really confused about how the binding works here. At first I have to clarify that I don't want a Navigation pane which is always visible like in the given example. I just want to use my MainView for navigation and each "SubView" should be able to go back to it's "parent" only.
Here's what I've got:
Project: APP
Class: App.xaml.cs
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
UI.View.Main.MainView app = new UI.View.Main.MainView();
UI.View.Main.MainViewModel viewModel = new UI.View.Main.MainViewModel(some dependencies);
app.DataContext = viewModel;
app.Show();
}
ViewModel Base Class
public abstract class BaseViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string Name {
get {
return _name;
}
set {
if (Name != value) {
_name = value;
OnPropertyChanged("Name");
}
}
}
private BaseViewModel _homePage;
public BaseViewModel HomePage {
get {
return _homePage;
}
set {
if (HomePage != value) {
_homePage = value;
OnPropertyChanged("HomePage");
}
}
}
public void OnPropertyChanged(string propertyName) {
PropertyChangedEventHandler temp = PropertyChanged;
if (temp != null) {
temp(this, new PropertyChangedEventArgs(propertyName));
}
}
}
MainViewModel
namespace SGDB.UI.View.Main {
public class MainViewModel : BaseViewModel {
private BaseViewModel _currentPageViewModel;
public BaseViewModel CurrentPageViewModel {
get {
return _currentPageViewModel;
}
set {
if (CurrentPageViewModel != value) {
_currentPageViewModel = value;
OnPropertyChanged("CurrentPageViewModel");
}
}
}
public List<BaseViewModel> PageViewModels { get; private set; }
public RelayCommand ChangePageCommand {
get {
return new RelayCommand(p => ChangeViewModel((BaseViewModel)p), p => p is BaseViewModel);
}
}
//Some Dependencies
public List<BaseViewModel> ViewPages { get; private set; }
public MainViewModel(some dependencies) {
HomePage = new HomeViewModel() { Name = "TEST" };
//assign dependencies
var uavm = new UserAdministration.UserAdministrationViewModel(_userUnitOfWork, _personUnitOfWork) {
Name = Resources.Language.Sys.UserAdministartionTitle
};
PageViewModels = new List<BaseViewModel>();
PageViewModels.Add(uavm);
ChangeViewModel(HomePage);
}
public void ChangeViewModel(BaseViewModel viewModel) {
CurrentPageViewModel = viewModel;
}
}
}
MainView
<Window x:Class="SGDB.UI.View.Main.MainView"
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:local="clr-namespace:SGDB.UI.View.Main"
xmlns:ua="clr-namespace:SGDB.UI.View.UserAdministration"
xmlns:home="clr-namespace:SGDB.UI.View.Home"
mc:Ignorable="d"
Title="MainView" Height="400" Width="800">
<Window.Resources>
<DataTemplate DataType="{x:Type home:HomeViewModel}">
<home:Home/>
</DataTemplate>
<DataTemplate DataType="{x:Type ua:UserAdministrationViewModel}">
<ua:UserAdministration/>
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding CurrentPageViewModel}"/>
HomeViewModel
public class HomeViewModel : BaseViewModel {
public RelayCommand TestCommand {
get {
return new RelayCommand((x) => MessageBox.Show(x.ToString()), (x) => true);
}
}
}
HomeView
<UserControl x:Class="SGDB.UI.View.Home.Home"
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:local="clr-namespace:SGDB.UI.View.Home"
xmlns:controls="clr-namespace:SGDB.UI.Controls"
xmlns:resx="clr-namespace:SGDB.UI.Resources.Language"
mc:Ignorable="d"
d:DesignHeight="400" d:DesignWidth="800">
<Grid>
<Grid.Resources>
<Style TargetType="controls:ModernButton">
<Setter Property="Margin" Value="1"/>
<Setter Property="FontFamily" Value="Bosch Office Sans"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Size" Value="155"/>
</Style>
</Grid.Resources>
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#26688B" Offset="1"/>
<GradientStop Color="#11354C" Offset="0"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Grid.Background>
<Grid.RowDefinitions>
<RowDefinition Height="60"/>
<RowDefinition Height="3*"/>
<RowDefinition Height="2*"/>
<RowDefinition Height="25"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="150"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" HorizontalAlignment="Center">
<StackPanel.Resources>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontFamily" Value="Bosch Office Sans"/>
</Style>
</StackPanel.Resources>
<TextBlock Text="{x:Static resx:Sys.ApplicationTitle}" FontSize="20" FontWeight="Bold" Margin="5"/>
<TextBlock Text="{x:Static resx:Sys.ApplicationSubTitle}" FontSize="12" FontWeight="Light"/>
</StackPanel>
<WrapPanel Grid.Row="1"
Grid.Column="0"
FlowDirection="LeftToRight"
HorizontalAlignment="Left"
Width="367">
<ItemsControl ItemsSource="{Binding DataContext.PageViewModels, RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:ModernButton Background="Dark"
Text="{Binding Name}"
Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="{Binding Name}" Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding HomePage}"/>
// This Button is always disabled although HomePage is of Type HomeViewModel which is based on BaseViewModel.
</WrapPanel>
</Grid>
My questions are:
Why does the HomeView knot that the HomeViewModel is it's ViewModel? I do not define it anywhere in my code.
Why does the Binding on the Name Property work but binding to the HomePage Property doesn't? Both of them are defined in the BaseViewModel class.
Update 1:
RelayCommand class:
public class RelayCommand : ICommand {
public event EventHandler CanExecuteChanged;
readonly Action<object> _action;
readonly Predicate<object> _predicate;
public RelayCommand(Action<object> action, Predicate<object> predicate) {
_action = action;
_predicate = predicate;
}
public RelayCommand(Action<object> action) {
_action = action;
_predicate = ((x) => true);
}
public bool CanExecute(object parameter) {
return _predicate(parameter);
}
public void Execute(object parameter) {
_action(parameter);
}
}
Update 2:
What's the actual problem?
<Button Content="{Binding Name}" Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding HomePage}"/>
The Content gets bound properly but the CommandParameter (HomePage) which should be of Type BaseViewModel won't get validated through the Command's CanExecute. Both the Properties, Name and HomePage are defined inside the BaseViewModel.
Update 3:
<Button Content="{Binding Name}" Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding DataContext.HomePage, ElementName=Test}"/>
In your there is the next lines:
<DataTemplate DataType="{x:Type home:HomeViewModel}">
<home:Home/>
</DataTemplate>
meaning that the visual form of HomeViewModel is Home.
Your Binding works fine, I think your problem is the command itself. I don't know what is RelayCommand but i think your bug is from there.
RelayCommand should be something like this:
public abstract class BaseViewModel : INotifyPropertyChanged
{
private ICommand _f1KeyCommand;
public ICommand F1KeyCommand
{
get
{
if (_f1KeyCommand == null)
_f1KeyCommand = new DelegateCommand(F1KeyCommandCallback, CanExecute);
return _f1KeyCommand;
}
}
/// <summary>
/// Fired if F1 is pressend and 'CanExecute' returns true
/// </summary>
private void F1KeyCommandCallback(object obj)
{
Console.WriteLine("F1KeyCommandCallback fired");
}
// ....
}
This class allows delegating the commanding logic to methods passed as parameters,and enables a View to bind commands to objects that are not part of the element tree:
public class DelegateCommand : ICommand
{
#region Data Members
private Action<object> execute;
private Predicate<object> canExecute;
private event EventHandler CanExecuteChangedInternal;
#endregion
#region Ctor
public DelegateCommand(Action<object> execute)
: this(execute, DefaultCanExecute)
{
}
public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
if (canExecute == null)
{
throw new ArgumentNullException("canExecute");
}
this.execute = execute;
this.canExecute = canExecute;
}
#endregion
#region Properties
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
this.CanExecuteChangedInternal += value;
}
remove
{
CommandManager.RequerySuggested -= value;
this.CanExecuteChangedInternal -= value;
}
}
#endregion
#region Public Methods
public bool CanExecute(object parameter)
{
return this.canExecute != null && this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
public void OnCanExecuteChanged()
{
EventHandler handler = this.CanExecuteChangedInternal;
if (handler != null)
{
handler.Invoke(this, EventArgs.Empty);
}
}
public void Destroy()
{
this.canExecute = _ => false;
this.execute = _ => { return; };
}
#endregion
#region Private Methods
private static bool DefaultCanExecute(object parameter)
{
return true;
}
#endregion
}
In your view:
<controls:ModernButton Background="Dark"
Text="{Binding Name}"
Command="{Binding F1KeyCommand"
CommandParameter="{Binding}"/>
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); }