Open Gamma36 opened 7 years ago
Hi Paul,
First thing to appreciate is that a window or usercontrol in WPF has a data context. When using an MVVM architecture this is set to your view model (which is typically a class implementing INotifyPropertyChanged).
If you look at the MapWindow user control file ($:\Lunatic.TelescopeControl\Controls\MapWindow.xaml.cs) you will see that an instance of the MapViewModel class is passed to the Window's constructor it is assigned to the Windows's DataContext property.
public MapWindow(MapViewModel viewModel)
{
InitializeComponent();
_ViewModel = viewModel;
this.DataContext = _ViewModel;
Now of look the MapWindow.xaml file you can find the definitions for the OK and Cancel button.
<StackPanel DockPanel.Dock="Bottom">
<Button Content="OK" Margin="0,0,0,10" Command="{Binding SaveChangesAndCloseCommand}" Width="80"/>
<Button Content="Cancel" Command="{Binding CancelChangesAndCloseCommand}" Width="80"/>
</StackPanel>
Notice the Command bindings on each of the definitions. The OK button is bound to a command SaveChangesAndCloseCommand
and the Cancel button is bound to the command CancelChangesAndCloseCommand
. These commands are defined on the view model, actually in this case they are defined on the base class of the view model LunaticViewModelBase
because saving and closing windows is something that we'll probably do a lot of.
Looking at ($:\Lunatic.Core\Classes\ViewModelBase.cs) you will see the following definition for the SaveChangesAndCloseCommand.
private RelayCommand _SaveChangesAndCloseCommand;
/// <summary>
/// Gets the SaveChangesAndCloseCommand.
/// </summary>
public RelayCommand SaveChangesAndCloseCommand
{
get
{
return _SaveChangesAndCloseCommand
?? (_SaveChangesAndCloseCommand = new RelayCommand(
() => {
if (OnSaveCommand()) {
// Don't need to do anything here as the assumption is that the properties
// and bound and therefore already saved.
if (this.SaveAndCloseAction != null) {
SaveAndCloseAction();
}
}
}));
}
}
Relay commands are defined like a property's getter. I'll explain the above a little...
We have a private member variable of type RelayCommand _SaveChangesAndCloseCommand
. I try always name property backing members the same as the property name but prefixed with an underscore.
The getter looks a little odd with the ??
but basically this means that the relay command is only actually defined the first time it is accessed (?? is the C# null-coalescing operator).
The bit that does the work is lamda expression passed to the RelayCommand constructor.
() => {
if (OnSaveCommand()) {
// Don't need to do anything here as the assumption is that the properties
// and bound and therefore already saved.
if (this.SaveAndCloseAction != null) {
SaveAndCloseAction();
}
}
}));
This is a parameter less relay command so the lamda parameters are empty. This command then calls a virtual OnSaveCommand() function (which can be overridden in derived class). If the OnSaveCommand returns true the command then looks to see if the viewmodel's SaveAndCloseAction has been hooked up (i.e. != null). If it is then the action is called.
Just rolling back and looking further down the constructor in MapWindow.xaml.cs you can see view model actions being hooked up.
// Hook up to the viewmodels close actions
if (_ViewModel.SaveAndCloseAction == null) {
_ViewModel.SaveAndCloseAction = new Action(() => {
this.DialogResult = true;
this.Close();
});
}
if (_ViewModel.CancelAndCloseAction == null) {
_ViewModel.CancelAndCloseAction = new Action(() => {
this.DialogResult = false;
this.Close();
});
}
This is a very brief introduction to relay commands and command binding in WPF. Hopefully as the project progresses I will get some more advanced examples in there for you to look at.
Moving from VB6 to a truly objected orientated language such as VB.NET or C# requires more than just learning a new language. To truly benefit from the effort you need to make a paradigm shift from thinking of programs as a procedural list of instructions to thinking of the application as being made up of objects with specific jobs to do. This is also the case when you move Winforms development to Windows Platform Foundation (WPF). While the underlying event model is still there the binding framework allows your code to be much more structured, re-usable and ultimately easier to maintain.
Chris, Thanks I get it all, but it feels a bit complicated. Fine for professionals like you and me, but amateur astronomers and VB6 developers might struggle. I guess we can always package things in simple way.
I have to admit I'm not a great fan of the expando property of Javascript. With C# being compiled I would expect one should never need to check:
< if (this.SaveAndCloseAction != null) { >
because the linker would sort this out. However I am a hardcore C++ engineering developer who links over 180 C++ libraries with tens of thousands of header files - so my expectations are skewed.
Is there anyway to force the declaration of SaveAndCloseAction()? At least in the base class. That would simplify the GUI code greatly. A blunt CheckNullFunctions() called by unit tests and within the main program might do the trick; it would avoid most null exception crashes.
Thanks Paul
I have been used to a "OnButton1" function type in MFC C++, with MFC's Document-View architecture. This is a simple architecture, very suitable for a small program like EQMOD.
I understand Lunaticsoftware uses MVVM Lite or Model-View-ViewModel. It offers more complex commands via the ICommand interface. Architecturally there are benefits, but it can also add complexity that keeps less confident developers contributing to the project. There are some very experienced astro-photographers who can contribute, in fact some wrote the original EQMOD using VB6. We need to keep access simple for them.
So, how will the button callbacks work? Can we see a code sample?
cheers Paul