Im new to the Caliburn Micro framework, and am working on an app that has a list view, which you can double click on an item to get take to a detailed view.
THis all works fine, and I current am doing the forward navigation by sending ChangePage messages, which the SHellView picks up and issues an ActivateItem command for the new page.
What I am unable to quite figure out is how to navigate back to a page and keep its state that it was in when you left it? I've read about the Conductor collection but not quite sure how it works in practice?
Does someone have an example where they send ChangePage messages using the eventAggregator and it is processed by the ShellView by checking if that page already exists first and if not create a new one?
Thanks!
UPDATE:
My change page message looks like this:
public class ChangePageMessage
{
public readonly Type _viewModelType;
public ChangePageMessage(Type viewModelType)
{
_viewModelType = viewModelType;
}
}
And my handling of the message in ShellView is:
public void Handle(ChangePageMessage message)
{
if (message._viewModelType == typeof(SearchResultsViewModel))
{
ActivateItem(new SearchResultsViewModel(_eventAggregator));
}
else if(message._viewModelType == typeof(DetailedDocumentViewModel))
{
ActivateItem(new DetailedDocumentViewModel(_eventAggregator));
}
else
{
//here
}
}
You could for example store the visited view models in a list or dictionary in the ShellViewModel and simply check whether an instance of the type message._viewModelType already exists in this collection when you receive a ChangePageMessage event.
If it exists, you return that instance. If not, then you create a new instance, add it to the list or dicionary, and return this one. Something like this:
private readonly Dictionary<Type, Screen> _viewModels = new Dictionary<Type, Screen>();
public void Handle(ChangePageMessage message)
{
if (_viewModels.TryGetValue(message._viewModelType), out Screen viewModel))
{
ActivateItem(viewModel);
}
else if (message._viewModelType == typeof(SearchResultsViewModel))
{
var vm = new SearchResultsViewModel(_eventAggregator);
_viewModels.Add(message._viewModelType, vm);
ActivateItem(vm);
}
else if (message._viewModelType == typeof(DetailedDocumentViewModel))
...
}
Related
I have a button attached to a command in the view model. This button deletes the rows current selected in the listview so I would like to show a messagebox of confirmation before proceeding. Is user click ok button (in messagebox) then command is executed, otherwise, if user click cancel button command attached is not called. Is it possible? If so how?
<Button Name="btnDelete" Command="{Binding DeleteRowsCommand}"/>
Another possibility is to call the command on click and in the view model through a property that is attached to a custom message box placed in the view, to make this custom messagebox visible when value of the property is true. But then how can I send back to the view model which button 'Ok' or 'Cancel' has been pressed?
The view model doesn't often need to know that there is a question for the user before the command is executed. If that's the case, you can create very simple custom button class to just show the message box and if the user click yes, execute the command (or do whatever).
public class YesNoButton : Button
{
public string Question { get; set; }
protected override void OnClick()
{
if (string.IsNullOrWhiteSpace(Question))
{
base.OnClick();
return;
}
var messageBoxResult = MessageBox.Show(Question, "Confirmation", MessageBoxButton.YesNo);
if (messageBoxResult == MessageBoxResult.Yes)
base.OnClick();
}
}
In XAML you can use the button like this:
<components:YesNoButton Content="Delete rows" Command="{Binding DeleteRowsCommand}" Question="Do you really want to delete rows?" />
EDIT: Another way to solve this is to define some MessageBoxService in the ViewModel layer and implement it in View layer. Interface could look like this:
public interface IMessageBoxService
{
void ShowError(string messageBoxText);
void ShowWarning(string messageBoxText);
void ShowInformation(string messageBoxText);
}
This way you can show message boxes directly from the VM without directly referencing WPF libraries.
Just use a MessageBox ;)
In the method which is routed to the DeleteRowsCommand use this
var result = MessageBox.Show("message", "caption", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
//your logic
}
Have a look at MessageBox Class for more information.
One of the possible (and in my opinion the cleanest) ways of doing this is to implement a service like DialogService, inject it into your ViewModel and call it when command is executed. By doing so you decouple your view and application logic so the ViewModel is completely unaware of how actually dialogs are shown and delegates all the work to the service. Here's an example.
First you create a dialog service which handles all the work of showing dialogs and returning their outcome:
public interface IDialogService
{
bool ConfirmDialog(string message);
}
public bool ConfirmDialog(string message)
{
MessageBoxResult result = MessageBox.Show(message, "Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question);
return result == MessageBoxResult.Yes ? true : false;
}
Then you make your ViewModel dependent on that service and inject it in the ViewModel:
public class MyViewModel : ViewModelBase
{
private readonly IDialogService _dialogService;
public MyViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
}
}
Finally in your command you call the service in your command to check whether the user is absolutely sure he wants to delete records or not:
public Command DeleteRecordsCommand
{
get
{
if (_deleteRecordsCommand == null)
{
_deleteRecordsCommand = new Command(
() =>
{
if (_dialogService.ConfirmDialog("Delete records?"))
{
// delete records
}
}
);
}
return _deleteRecordsCommand;
}
}
I'm using the SimpleMVVM Toolkit.
I have a view (manage_view) with multiple buttons that will navigate (set a frame's source) to new views (manage_import_view, manage_scanners_view, etc). Each view has it's own VM.
For each of the views I set the datacontext to the VM by using a locator. The locator injects a ServiceAgent into the VM.
The problem is that when I navigate to the other views, the state of the previous view is lost. For instance I'd do an import on the Manage_Import_View and bind to properties on the VM. When I navigate to Manage_Scanners_View and then back to the Manage_Import_View, the properties that I bound to is lost.
I understand what is happening but Im not sure how to resolve it. How do I keep the state of the views when switching between them?
Looking forward to your thoughts on this.
(I've searched Switching between views according to state but it's not exactly what I need.)
Edit
My locator
public ImportViewModel ImportViewModel
{
get
{
IIntegrationServiceAgent sa = new IntegrationServiceAgent();
return new ImportViewModel(sa);
}
}
In my view's XAML I set the datacontext
DataContext="{Binding Source={StaticResource Locator}, Path=ImportViewModel}"
Navigation is like so
private void Navigate(string pageName)
{
Uri pageUri = new Uri("/Views/" + pageName + ".xaml", UriKind.Relative);
this.SelectedPage = pageUri;
this.SelectedPageName = pageName;
}
I have a completion callback once import is complete. This sets the props that my view binds to - these are the ones that are reset after switching views.
private void ImportCompleted(IntegrationResult intresult, Exception error)
{
if (error == null)
{
_errorCount = intresult.Errors.Count;
ErrorList = intresult.Errors;
ResultMessage = intresult.Message;
ErrorMessage = (errorList.Count == 1 ? "1 error" : errorList.Count.ToString() + " errors");
Notify(ImportCompleteNotice, null); // Tell the view we're done
ShowErrorDialog(importType);
}
else
NotifyError(error.Message, error);
IsImportBusy = false;
}
This seems clunky to me. I am not entirely sure why this is happening but I can guess... You are loading the SelectedPage from Uris each time they are requested, this will set and parse the XAML each time they are loaded which will effect your bindings. Here's what I would do:
First on the application start-up, load all of the Views into a view list
private Dictionary<string, Uri> viewUriDict;
private List<string> viewNameList = new List<string>()
{
"ViewA",
"ViewB"
};
// The main View Model constructor.
public MainViewModel()
{
viewUriDict = new Dictionary<string, Uri>();
foreach (string s in viewNameList)
viewUriDict.Add(s, new Uri("/Views/" + s + ".xaml", UriKind.Relative);
this.SelectedPageName = viewNameList[0];
}
private string selectedPageName;
public string SelectedPageName
{
get { return this.selectedPageName; }
set
{
if (this.selectedPageName == value)
return;
this.selectedPageName = value;
this.SelectedPage = this.viewUriDict[this.selectedPageName];
OnPropertyChanged("SelectedPageName"); // For INotifyPropertyChanged.
}
}
private Uri selectedPage;
private Uri selectedPageName
{
get { return this.selectedPage; }
set
{
if (this.selectedPage == value)
return;
this.selectedPage = value;
OnPropertyChanged("SelectedPage"); // For INotifyPropertyChanged.
}
}
So now the Uri list is cached in your main window/app. Navigation would then become
private void Navigate(string pageName)
{
this.SelectedPageName = pageName;
}
or by merely setting this.SelectedPageName = "PageX".
The second thing I would do is lazily instantiate the InportViewModel service agent. I am not sure how this is called, but I would not re-create the service agent on every call...
private IIntegrationServiceAgent sa;
public ImportViewModel ImportViewModel
{
get
{
if (sa == null)
sa = new IntegrationServiceAgent();
return new ImportViewModel(sa);
}
}
This may resolve your issue, or might not. Either way I hope it is of some value. If I were you, I would look at using Prism to do this type of thing, although it might be overkill if this is a small project.
I hope this helps.
For anyone who've experience the same puzzle, I've found the answer here and here .
tonysneed's reply on the second link explains it.
I have a ShellViewModel which loads a Modal Dialog. The Dialog's ViewModel has its OnActivate() override, where it gathers the data to be displayed on the Dialog. I would like to know how can we ask the WindowManager to cancel its ShowDialog based on a condition in OnActivate of the ViewModel backing the dialog.
For example, lets say that I have following code in ShellViewModel which tries to load a modal dialog based on StationOpenViewModel
public class ShellViewModel : Conductor<object>, IShell, IHandle<ConnectionChangedEvent> {
public void ShowOpenStationPage() {
StationOpenViewModel viewModel = container.GetExportedValue<StationOpenViewModel>();
windowManager.ShowDialog(viewModel);
}
...
}
and here is to code of OnActivate override of the StationOpenViewModel
public class StationOpenViewModel : Screen {
...
protected override void OnActivate() {
try {
using (StationRepository stationRepository = new StationRepository()) {
//code to get Station Data
}
catch (Exception ex) {
//Here I have no data, so there is no point in showing the window.
//How to cancel showDialog() for this viewModel
}
...
}
So in the above code, if I get Exception in OnActivate override, I don't have any Station data to show and I would like to cancel the showDialog() for the StationOpenViewModel. I tried using TryClose(), but if I do so, the WindowManager.ShowDialog() throws exception saying that the operation is invalid.
In summary, if I call WindowManager.ShowDialog() for a dialog backed by some ViewModel, then in that ViewModel how do I cancel the ShowDialog() operation.
The ShowDialog() implementation in CM source is:
public virtual void ShowDialog(object rootModel, object context = null, IDictionary<string, object> settings = null)
{
var view = EnsureWindow(rootModel, ViewLocator.LocateForModel(rootModel, null, context));
ViewModelBinder.Bind(rootModel, view, context);
var haveDisplayName = rootModel as IHaveDisplayName;
if(haveDisplayName != null && !ConventionManager.HasBinding(view, ChildWindow.TitleProperty)) {
var binding = new Binding("DisplayName") { Mode = BindingMode.TwoWay };
view.SetBinding(ChildWindow.TitleProperty, binding);
}
ApplySettings(view, settings);
new WindowConductor(rootModel, view);
view.Show();
}
full source here:
http://caliburnmicro.codeplex.com/SourceControl/changeset/view/ae25b519bf1e46a506c85395f04aaffb654c0a08#src/Caliburn.Micro.Silverlight/WindowManager.cs
It doesn't look like there is a good way to do this with the default implementation. You should probably implement your own WindowManager and subclass the original implementation
The WindowConductor in the above code file is responsible for the lifecycle of the window, therefore and additional interface which your VMs can implement would work well:
public interface ICancelActivate
{
public bool ActivationCancelled { get };
}
Then just change your MyWindowConductor implementation to something like:
public MyWindowConductor(object model, ChildWindow view)
{
// Added this field so the window manager can query the state of activation (or use a prop if you like)
public bool ActivationCancelled;
this.model = model;
this.view = view;
var activatable = model as IActivate;
if (activatable != null)
{
activatable.Activate();
}
// Added code here, check to see if the activation was cancelled:
var cancelActivate = model as ICancelActivate;
if(cancelActivate != null)
{
ActivationCancelled = cancelActivate.ActivationCancelled;
if(ActivationCancelled) return; // Don't bother handling the rest of activation logic if cancelled
}
var deactivatable = model as IDeactivate;
if (deactivatable != null) {
view.Closed += Closed;
deactivatable.Deactivated += Deactivated;
}
var guard = model as IGuardClose;
if (guard != null) {
view.Closing += Closing;
}
}
then to stop the view from showing:
// This is in 'ShowDialog' - you can override the default impl. as the method is marked virtual
ApplySettings(view, settings);
// Get a ref to the conductor so you can check if activation was cancelled
var conductor = new MyWindowConductor(rootModel, view);
// Check and don't show if we don't need to
if(!conductor.ActivationCancelled)
view.Show();
Obviously I've just thrown this together so it might not be the best way, and I'd look carefully at where this leaves the state of your application
Your VMs just implement this:
public class StationOpenViewModel : Screen, ICancelActivation {
private bool _activationCancelled;
public bool ActivationCancelled { get { return _activationCancelled; } }
...
protected override void OnActivate() {
try {
using (StationRepository stationRepository = new StationRepository()) {
//code to get Station Data
}
catch (Exception ex) {
_activationCancelled = true;
}
...
}
... of course there may be better ways for you to check if you need to open a VM in the first place - I'm not sure what they would be but still, worth thinking about
Edit:
The reason I didn't just do this in the WindowManager...
new WindowConductor(rootModel, view);
var cancel = rootModel as ICancelActivation;
if(cancel == null || !cancel.ActivationCancelled) // fixed the bug here!
view.Show();
Is twofold - 1: you are still letting the WindowConductor add Deactivate and GuardClose hooks even though they should never be used, which may lead to some undesirable behaviour (not sure about reference holding either - probably ok with this once since nothing holds a ref to the conductor/VM)
2: it seems like the WindowConductor which activates the VM should be responsible for handling the cancellation of activation - ok it does mean that the WindowManager needs to know whether to show the VM or not, but it seemed a more natural fit to me
Edit 2:
One idea might be to move view.Show() into the conductor - that way you can cancel the activation without needing to expose details to the manager. Both are dependent on each other though so it's the same either way to me
I've got a composite collection. After modifications on its items from code behind I want the View to get updated. But I don't know how to notify the view. I tried the INotifyCollectionChanged, but it didn't work for me.
protected ObservableCollection<ScriptParameterComboItem> cItems
public virtual CompositeCollection CItems
{
get
{
return new CompositeCollection {new CollectionContainer {Collection = cItems}};
}
}
public void ConvertValue(params object[] parameters)
{
string newAverageOption = DisplayValueConverter.Convert(1, parameters);
var enumItem = cItems[1];
enumItem.Value = newAverageOption;
RaiseCollectionChanged("CItems");
}
protected void RaiseCollectionChanged(string property)
{
if(CollectionChanged != null)
CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add));
}
Your ScriptParameterComboItem class must implement INotifyPropertyChanged. So when changing it's properties, listeners will be notified. Using ObservableCollection helps listeners to be notified when something is added to the collection or removed from. Not changing the actual data within every single item.
In each view
public partial class View2 : UserControl, IRegionMemberLifetime, INavigationAware
{
public bool KeepAlive
{
get { return false; }
}
bool INavigationAware.IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
void INavigationAware.OnNavigatedFrom(NavigationContext navigationContext)
{
// Intentionally not implemented.
}
void INavigationAware.OnNavigatedTo(NavigationContext navigationContext)
{
this.navigationJournal = navigationContext.NavigationService.Journal;
}
}
Initialize:
container.RegisterType<object, View1>("View1");
container.RegisterType<object, View2>("View2");
regionManager.RequestNavigate("Window1", new Uri("View1", UriKind.Relative));
regionManager.RequestNavigate("Window2", new Uri("View2", UriKind.Relative));
I am following the developer guide, it does not change the view if view exists.
Are you sure the view gets populated by the container?
I would suggest you to provide a callback for the RequestNavigate method, so you'll be able to track what happens with your view thru the NavigationResult:
regionManager.RequestNavigate
(
"Window1",
new Uri("View2", UriKind.Relative),
(NavigationResult nr) =>
{
var error = nr.Error;
var result = nr.Result;
// put a breakpoint here and checkout what NavigationResult contains
}
);
I have seen that if I implement IConfirmNavigateRequest and do not call continutationCallback(true), the navigation fails quietly.
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
//***Should have actual logic here
continuationCallback(true);
}
While this may not be your case, I figured this out by debugging through the Prism code. I would suggest you do this to figure out your issue. Delete the references to the following in each relevant project.
Microsoft.Practices.Prism
Microsoft.Practices.Prism.Interactivity
Microsoft.Practices.Prism.MefExtensions
Microsoft.Practices.Prism.UnityExtensions
Then add the projects from the PrismLibrary DeskTop, Silverlight or Phone directory (where you installed PRISM). Then reference these projects.
This is your problem:
bool INavigationAware.IsNavigationTarget(NavigationContext navigationContext) => true;
If you want a new view to be created and added to your region each time you call RequestNavigate(), IsNavigationTarget() must return false instead of true.