I have one parent conductor. I want to show first view-model inside it. Then after first one is closed (i.e. some operation is done), I want to show a different view-model.
I'm using Caliburn.Micro.Contrib, where a ConductResult displays child VM in a Conductor. It has a cool extension method AfterClosingDo, which runs a coroutine after that child was deactivated and closed.
However, when I run another ConductResult using AfterClosingDo, basically this happens:
first child VM is closed
Deactivated event occurs, ConductResult runs AfterClosing action
in AfterClosing, I open second child VM using ConductResult in parent Conductor
second child VM is properly activated
however, the deactivation of first child VM still isn't completed, and null item is set as active in the Conductor
First VM basically shows progress of a load operation, second VM shows actual data. After the load is complete, I want to show the data in parent Conductor (using second VM, of course).
So, my question: is there a clean way to do this in Caliburn.Micro, preferably by not overriding default behavior of Conductor, Screen, etc.
I was thinking of using EventAggregator, though I'm not sure, if it's the best solution.
I had a very similar issue where I had a conductor opening a child VM, then a confirmation box would pop-up saying "do you want to close" and had fired a callback for the original items CanClose method which had the same sort of effect as you described.
The popup VM would close, but in closing it would fire a callback which was supposed to close the first VM.
My conductor ended up re-activating the original VM which was annoying. The order of events was:
Open VM 1
Try Close VM 1
CanClose guard method fired
Popup VM 2 (using same conductor) in CanClose of VM 1
Confirm button on VM 2 is clicked
Confirm button fires callback for CanClose and closes VM1
VM2 Closes
Conductor remembers that VM 1 was active before VM 2 so re-opens VM 1 after it's closed
In the end I just implemented an interface which fired after the close.
Child items which have work to do after they have closed implement the interface (IAfterClose)
Then I provided an override for DeactivateItem on the conductor:
public override void DeactivateItem(IScreen item, bool close)
{
var afterClose = item as IAfterClose;
base.DeactivateItem(item, close);
if (afterClose != null && close)
afterClose.AfterClose();
}
This made sure that the callback wasn't fired too early. Not sure if this will benefit you (since I've not used the contrib library) but it might give you some ideas.
The only downside to this was that I had to make a fix to DefaultCloseStrategy as when the callback would fire it would throw a null reference exception in there. The fix I applied seemed to cause no ill-effect but I've not really looked at why the null ref exception was thrown.
I couldn't find any other way to do this since the last event that fires seems to be the deactivation events and they are still to early.
Related
I have a selection of TextBoxes that a user fills in when they wish to note that they have had contact with another person. Most of the TextBoxes are imply filled in by typing into them. However, for one of them I would like the user to be able to select from a list of People that appears when they click on a button.
This is where I am having problems. So far I have just made a DataGrid appear and handled it's SelectionChanged method to fill in the TextBoxes text property. This has worked fine, however now there is not enough space on the current page to show an entire DataGrid with all the people they can select from.
I've decided to show the People in a separate, smaller Window that appears when the user clicks a Button. The issue I have is that when the user selects the Person they wish to mark the contact for in the new Window, I have no idea how I can notify the original Window that a Person has been selected, close the new smaller Window and fill in the appropriate TextBox on the original Window.
What would be the most intuitive way to fill in the TextBox on the original Window, based on the selection on the Window that opens?
I would use delegates,which call a function of the original window and parse the changed variable with it. So you know when the user clicked something and you can directly react to this "event".
Link:
https://msdn.microsoft.com/en-us/library/ms173171.aspx
If you use a framework like Galasoft's MVVM Light (http://www.galasoft.ch/), they have a messenger system just for this purpose. It allows you to "broadcast" messages that can be "received" by any other part of the application
This is when considering using Domain, Model, Presentation (Winforms/WPF version of MVC formatting) to do your app.
You can have each form as its own class, well they are their own class. Create each form class but add some public members to it if the controls are private. Have them have "get" properties only and to return the values of whatever controls or variables are in that form. Your main form will be the controlling form. All forms will be handled by the main form so when you open it, it is a class the main form can access.
Now, if I remember (been doing more MVC and not any Winforms lately) I believe if you use the ShowDialog() method it will freeze the main thread so when you close out the main form you can continue and read in public members you have in your forms class you opened. Synchronous I believe it runs as. If you use just Show() the thread will keep on trucking, asynchronous. With asynchronous you may then have to use a main form in your startup code so there is always a window there but subscribe to the close event of your forms and have a method that can grab those public members out. Be sure to instantiate the extra forms at the root of the main class so it doesn't fall out of scope when it exists the method that calls it. You may even be able to make the method that calls is a async call and have an await before the command that runs the Show method on the form.
Summary, treat each form as its own class but add public members that can read the values from the controls and/or variables you want. Read that data from the class when it closes via an event or synchronously when the thread closes out from the form closing. The form closing doesn't discard the object, just the visualization of the form.
Oh, if you are passing info from the main form to a child for you are opening, either add a constructor for that form class that takes your input as a model or values to fill in the appropriate variables or forms before showing it or create a public property you can put your values you want to send in before showing the class.
Remember, everything is a class, once you look at it as such and treat it as such, the answer will come. :-)
I should warn, I am a long winded explainer.
At work putting all this down from memory so some errors may exist. Let me know if there are.
I think the problem is to access the controls of the main window, isn`t it?
You can define an event of changing user`s choise and access MainWindow control by using the following construction:
((MainWindow)Application.Current.MainWindow).MyTextBox
I'm having a strange issue, and im pretty much at my wits end trying to work it out.
I have a Conductor which activates and deactivates viewmodels used for editing data, these view models implements screen and use OnDeactivate to ensure that any changes are saved before closing.
However for some reason, OnDeactivate in one of my ViewModels is never called, even tho i can see it being passed to DeactivateItem of the conductor.
To do this have the following in my conductor:
private void SwitchScreen(Screen viewModel)
{
DeactivateItem(ActiveItem, true);
ActivateItem(viewModel);
NotifyOfPropertyChange(() => ProjectActionRegion);
}
public override void DeactivateItem(IScreen item, bool close)
{
base.DeactivateItem(item, close);
NotifyOfPropertyChange(() => ProjectActionRegion);
}
This ensures that when TryClose is called the region is correctly updated. The SwitchScreen is called each time a selection is made on a datagrid, loading the viewmodel. I can see that Deactivate item is called when i change selection, and i can see its passing the correct viewmodel into that method.
However OnDeactivate is still never called, and i have no idea why :/
protected override void OnDeactivate(bool close)
{
System.Windows.Forms.MessageBox.Show("SAVE ME!");
}
Edited to remove incorrect code (the base. was a mistake, this is my actual code)
EDIT:
I've just realized what the difference between the working versions and the broken version is. I have a view/viewmodel that works as a conductor, this works fine. However inside that viewmodel i load a second view/viewmodel that also works as a conductor, this one fails to work, i wonder if it has to do with with being inside another conductor (but not actually handled by that conductor, just loaded into that viewmodel)
For screen life-cycle to function correctly, all of the view models in your view hierarchy must be conducted. You should make your child conductor an active screen of your parent conductor.
You can either do this by making it the active item of the parent conductor, or by using the ConductWith method on the child conductor, passing in a reference to the parent conductor.
I am currently trying to implement a navigation scheme that closely resembles that of the Internet Explorer app on Windows Phone 8.
The IE app can have multiple tabs that the user can switch between. Each of these tabs has its own history. Hitting the Back Button on the phone takes you to the previous page in that tab's Navigation history (Not the PhoneApplicationFrame.BackStack). If there are no previous pages, the back button takes you to the previous opened tab or, if none, exits the app.
Why this is troubling me
Application.RootVisual can only be set once. So you can't have two PhoneApplicationFrames, each with its own BackStack, to swap RootVisual between the two.
You cannot traverse the BackStack (it is a Stack, after all). Can only call GoBack(). Calling GoForward() will throw an Exception.
PhoneApplicationFrame.GoBack() removes entries from the BackStack which can only be added again through the PhoneApplicationFrame.Navigate(...) method. So, manipulating the BackStack is a no-go.
Bright Ideas
Keep a Dictionary<enum, List<string>> which is updated with each call to a custom NavigationService.Navigate(tabTypeEnum, uriString, params). This will keep the Navigation history for each tabType, allowing us to possibly Navigate through the current Tab's history when the BackKeyPress event is handled. Bad thing is, calling Navigate(...) to go to previous pages (instead of GoBack) will add to the BackStack. So requires maintenance that hurts my brain right now.
Create a custom NavigationAwareTabPage : PhoneApplicationPage, which keeps track of its own navigation history and fakes navigation by animating a transition when its Content is changed. The only time we call a true Navigate is when we switch from one tab to another. (I think this is what the IE app does.) And the BackKeyPress would have to look like below.
This:
void RootFrame_BackKeyPress(object sender, CancelEventArgs e)
{
var rootFrame = sender as PhoneApplicationFrame;
if (rootFrame.CanGoBack)
{
// Get the NavigationAwarePage
var navAwarePage = rootFrame.Content as NavigationAwareTabPage;
if(navAwarePage.CanGoBack())
{
// This method "navigates" to the next page
// by changing the navAwarePage.Content
navAwarePage.GoBackToPreviousPage();
e.Cancel = true;
}
}
}
Has anyone been down this road?
All the magic of how ReactiveUI overrides the Back button is here:
https://github.com/reactiveui/ReactiveUI/blob/master/ReactiveUI.Mobile/WP8AutoSuspendApplication.cs#L91
The way that this works in ReactiveUI is that there is a content control named RoutedViewHost that is listening to the Back being signaled (you can do whatever you want in response to the hardware Back button and cancel the default action). ReactiveUI maintains its own ViewModel-based back stack and manipulates that instead of using WP8s, and you never call WP8s navigation methods.
This effectively means that, from WP8's perspective, there is only ever one page in the entire application. WP8 really wants to create that page itself, and it's specified in WMAppManifest.xml.
Don't try to participate in WP8's Frame system, it really wants to work its own way and you won't be able to convince it otherwise.
One last important thing, if you're at the bottom of your back stack, you must allow the default Back action to happen (i.e. what WP8 wanted to do, take you out of the app). Otherwise you'll probably fail Certification and you're Doing It Wrong™.
I have a Caliburn.Micro shell (i.e., an empty XAML view to contain other views) rendered by a Conductor ViewModel. From there I open a Screen via:
ActivateItem(...)
Usually from the newly displayed dialog the user can perform some operations and click buttons (OK, Cancel, Build....) which should each transition to another screen (in the shell).
public MyDialog : Screen
{
public void Ok()
{
// TODO: Somehow tell the conductor or called of this class about this action.
}
}
What are good ways to achieve these kind of dialog action/message screen transitions?
Simple .NET events are possible -- Wouldn't that be a bad idea?
CM IEventAggregator should also work by changing the view
Checking from the shell Conductor the ViewModel result once it has been closed via TryClose() -- Should be possible, just don't know how to achieve this in CM.
Reference the shell Conductor instance from that screen (via IoC or directly) -- That seems strong coupling.
Could you please advise.
My preferred approach is to use the EventAggregator to facilitate messaging between VMs.
This works especially well when you have multiple windows which are listening for a certain type of event (e.g. a Visual Studio style interface with multiple tool windows which may show context sensitive properties), however it sounds a little overkill for this implementation. Of course the advantages are still a good loose coupling between VMs and a lack of events (which is a good thing!)
It sounds like you want a modal dialog to popup and present an option, and then activate another screen once the first one has returned.
You can attach an event handler to the Deactivated event in the child VM which will fire when an item deactivates. It also passes a boolean in the arguments to notify if the item which deactivated was closed - you can check for this and activate the corresponding screen in your conductor.
e.g.
this.Deactivated += new EventHandler<DeactivationEventArgs>(WorkspaceViewModel_Deactivated);
void WorkspaceViewModel_Deactivated(object sender, DeactivationEventArgs e)
{
if(e.WasClosed) // raise some event
}
Then pass an event up to the conductor, I wouldn't really go the event route for this. This couples the VMs one-way so it may not be the most flexible solution
The alternative is to fire a message via the event aggregator to tell the conductor it needs to open a different window when the child VM closes. The same method can be used but it's decoupled
this.Deactivated += new EventHandler<DeactivationEventArgs>(WorkspaceViewModel_Deactivated);
void WorkspaceViewModel_Deactivated(object sender, DeactivationEventArgs e)
{
if(e.WasClosed) MainConductor.EventAggregator.Publish(new ActivateWindowMessage(typeof(SomeVM));
}
In my application there are only 2 windows — win_a & win_b, on each of these windows there is button that call another window, e.g. click on btn1 of win_a will call win_b, click on btn2 of win_b will show win_a.
Desired behaviour:
1. Only one instance of object is premitted at the same time, e.g. situation, where 2 instances of win_a running at the same time is not permitted.
When you click on button that calls windows that already exist this action will only change a focus to needed window.
If you call a window that previously had been created, but after this has been closed this action will create a new instance of this window. E.g. there are 2 running windows. you close one of them and after try to call this window back, so related button will create it.
How to write it in WPF (XAML + C#). For the moment I wrote a version that can create a lot of instances of the same window (no number of instances control implemented), but I want to see only one instance of the same window, as we can see it in a lot of applications.
Example of my code:
Window win = new Window();
win.Show();
Thanks.
first you need 2 references on each other window. on button click
you need to check one reference.
say in win_a
//win_b is a member on Windows_a class
if(_win_b.IsVisible())
{
// set focus on it
}
else
{
//show win_b
}
make the same for windows_b
I would suggest a different approach:
make a singleton class, that holds a list of tuples List>
when creating windows you can check if the window is in the collection or not.
if the collection holds a window you can set it activ win.Activate(),
else you can create it and add a reference to the collection list.add(tuple(win,"windowA"))
3.finally on the windows that you can add to the collection, on closing you need to remove the window from the singletons list, you can do this handling the Close event of the window
i don't have the code i wrote here, but i hope it helps.