How to Cancel windowManager.ShowDialog() from ViewModel - wpf

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

Related

How to navigate back to a previous view in Caliburn Micro WPF?

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

Retain UI state from ViewModel when switching Views

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.

asynchronous UI update from ViewModel in WPF

I am having a problem with getting data from db and showing in UI asynchronously.
I am using MVVM light, when I click the button, action is triggered in ViewModel:
private void SearchQuery(string query)
{
_redisModel.GetFriendsListAsync(query);
}
At some point GetFriendsListCompleted is called by background thread notifing viewmodel that job is done.
At this point I need to update ListBox ItemSource. But when I try to update is I get
“The calling thread cannot access this object because a different thread owns it”
I have tried Dispatcher.CurrentDispatcher.Invoke(),App.Current.Dispatcher.Invoke() and different magic, but it still doesn’t work.
I tried to give UI dispatcher to ViewModel and then call it from there - didn't work.
private string filterText = string.Empty;
public string FilterText
{
get { return filterText; }
set
{
filterText = value;
this.RaisePropertyChanged(() => this.FilterText);
this.FriendsList.View.Refresh(); // Here where exception is happening.
}
}
I tried to change this line to
Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
() =>this.FriendsList.View.Refresh())); - still the same.
I am using Telerik ListBox to display items. FriendList is CollectionViewSource(http://www.telerik.com/help/wpf/radlistbox-overview.html). It works when I use Telerik example from WPF Control Examples. Problems start to occur when I use my async methods.
Type of view is System.ComponentModel.ICollectionView it is used for Filtering and Grouping.
I have also tried to just assign ObservableCollection to Items property of the ListBox and it doesn't work either.
A bit more details on how _redisModel.GetFriendsListAsync works:
In the end(after all chain of calls) it ends up here:
public GetAsyncResult(Func<T> workToBeDone, Action<IAsyncResult> cbMethod, Object state)
{
_cbMethod = cbMethod;
_state = state;
QueueWorkOnThreadPool(workToBeDone);
}
ThreadPool.QueueUserWorkItem(state =>
{
try
{
_result = workToBeDone();
}
catch (Exception ex)
{
_exception = ex;
}
finally
{
UpdateStatusToComplete(); //1 and 2
NotifyCallbackWhenAvailable(); //3 callback invocation
}
});
In viewmodel I have method:
private void GetFriendsListCompleted(object sender, ResultsArgs<Friend> e)
{
if (!e.HasError)
{
var curr = e.Results;
if (curr != null)
{
this.FriendsList= new CollectionViewSource();
this.FriendsList.Source = list;
this.FriendsList.Filter += this.FriendFilter;
FilterText = "";
Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
() => this.FriendsList.View.Refresh()));
}
}
Can anybody please help me with this ?
Thank you
You are creating CollectionViewSource in one thread and refreshing that in another thread (dispatcher thread). Update your GetFriendsListCompleted to
private void GetFriendsListCompleted(object sender, ResultsArgs<Friend> e)
{
if (!e.HasError)
{
var curr = e.Results;
if (curr != null)
{
Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
() => {
this.FriendsList= new CollectionViewSource();
this.FriendsList.Source = list;
this.FriendsList.Filter += this.FriendFilter;
FilterText = "";
this.FriendsList.View.Refresh();
}));
}
}
}
You haven't shown any of the code that's actually running on the background thread on completion but I'm guessing that in it you're creating a collection object that you're then trying to assign to your CollectionView. When the CV tries to update (on the UI thread) from your Refresh call it would then try to use the collection that's owned by the other thread.
If you include the relevant code it would be easier to say for sure.

Binding ComboBox and ObservableCollection<KeyValue> in wpf

in My OpenViewModel i collect data:
private ObservableCollection<KeyValue> availableData;
public ObservableCollection<KeyValue> AvailableDatas
{
get { return availableData; }
set
{
if (value != availableData)
{
availableData= value;
NotifyOfPropertyChange("AvailableDatas");
}
}
}
method for collecting data:
public ObservableCollection<KeyValue> CollectData()
{
ConnectorClient client = null;
try
{
client = webservice.GetClient();
AvailableDatas = client.GetDatas();
client.Close();
}
catch (Exception ex)
{
webservice.HandleException(ex, client);
}
return AvailableDatas;
}
How to call the method CollectData in wpf and fill my COmboBox?
thx
You might simply call the method the first time the AvailableDatas property is accessed (e.g. from a binding in XAML):
private ObservableCollection<KeyValue> availableData;
public ObservableCollection<KeyValue> AvailableDatas
{
get
{
if (availableData == null)
{
availableData = CollectData();
}
return availableData;
}
set
{
if (value != availableData)
{
availableData = value;
NotifyOfPropertyChange("AvailableDatas");
}
}
}
Then you should change the CollectData method in a way that is does not also set the property:
public ObservableCollection<KeyValue> CollectData()
{
ConnectorClient client = null;
ObservableCollection<KeyValue> data = null;
try
{
client = webservice.GetClient();
data = client.GetDatas();
client.Close();
}
catch (Exception ex)
{
webservice.HandleException(ex, client);
}
return data;
}
You could override the OnActivated() event assuming you are using an IScreen implementation and load data in there, or just do it in the constructor or a custom Initialise method if you want to roll your own (or in the property accessor as someone has already said).
You can also use coroutines if you want some visual context for the user and a better tie in with CM actions
There is a nice simple implementation of a Loader class here which helps provide visual context to the user:
https://caliburnmicro.codeplex.com/wikipage?title=IResult%20and%20Coroutines&referringTitle=Documentation
This searches the visual tree for a BusyIndicator control and activates it whilst the content is loading e.g. ...
public class SomeViewModel : Screen
{
protected override void OnActivate()
{
RefreshData();
}
public void RefreshData()
{
Coroutine.BeginExecute(LoadData(), new ActionExecutionContext() { Target = this });
}
public IEnumerable<IResult> LoadData()
{
yield return Loader.Show("Loading Data...");
yield return new LoadSomeDataRoutine(client.GetDatas);
yield return Loader.Hide();
}
}
The reason to have a RefreshData method is that this also allows you to bind CM actions and allows the coroutine can grab more contextual information.
Obviously you have less need to worry about the async->sync benefits this gives in Silverlight because you are using WPF (but it still applies to async web service calls), however it still has many benefits and it also helps you to write reusable routines which become part of your application framework (e.g. some level of error handling/logging encapsulated in the IResult implementation etc)
You also mentioned filling the combobox... all you would need to do in CM is place a combobox on your control, and set it's Name property to the name of the property on your VM:
public class SomeViewModel : Screen
{
public ObservableCollection<MyObject> MyProperty { //blah blah... }
}
<UserControl .... blah>
<ComboBox x:Name="MyProperty" />
</UserControl>
This will fill the combobox with the items. You will still need to set the binding for SelectedItem/SelectedValue
I assume you know this already though - if not CM has some decent documentation:
https://caliburnmicro.codeplex.com/wikipage?title=Basic%20Configuration%2c%20Actions%20and%20Conventions&referringTitle=Documentation

Prism RequestNavigate does not work

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.

Resources