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.
Related
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))
...
}
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 have a need of one DependencyProperty from a View in my ViewModel constructor:
My problem: MEF wouldn't SatisfyImports() 'because it is marked with one or more ExportAttributes' (that is the exception)
This is the code structure for the VIEW:
public class MyView : UserControl
{
[Export(MethodTypes.ChartType)]
public Charts MyChartType
{
get
{
object k = GetValue(ChartTypeProperty);
Charts f = (Charts)Enum.Parse(typeof(Charts), k.ToString(), true);
return f;
}
set
{
SetValue(ChartTypeProperty, value);
}
}
[Import(ViewModelTypes.GenericChartViewModel)]
public object ViewModel
{
set
{
DataContext = value;
}
}
public MyView()
{
InitializeComponent();
if (!ViewModelBase.IsInDesignModeStatic)
{
// Use MEF To load the View Model
CompositionInitializer.SatisfyImports(this);
}
}
}
and the VIEWMODEL:
[PartCreationPolicy(CreationPolicy.NonShared)]
[Export(ViewModelTypes.GenericChartViewModel)]
public class GenericChartViewModel
{
[ImportingConstructor]
public GenericChartViewModel([Import(MethodTypes.ChartType)] Charts forChartType)
{
string test = forChartType.ToString();
}
}
Please give me any hints on this or maybe suggest a better solution for passing parameters through mef
In my case, I would need to pass only dependecyproperty's for now...
Thanks
Your work around isn't really good.. can't you remove the export from ChartTypes and pass it manually to whoever wants it? I presume the viewmodel is only one insterested in it..
I managed to put this through !
Instead of the code in the default constructor, I use:
void MyView_Loaded(object sender, RoutedEventArgs e)
{
if (!ViewModelBase.IsInDesignModeStatic)
{
var catalog = new TypeCatalog(typeof(GenericChartViewModel));
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
}
}
and the dependencyproperty value is correctly propagated to the ViewModel
(must do this after control is loaded, or the property will have its default value)
However, I would be very grateful if someone could:
tell me how generate a catalog from another non-referenced assembly?
Thanks
I have a silverlight 3 application with the latest Caliburn RTW.
I have a button with the following caliburn property in XAML:
PresentationFramework:Message.Attach="ContainerCommand ClassesCommand()"/>
In my module.cs I have :
_container.RegisterType(typeof(ClassesCommand), new ContainerControlledLifetimeManager());
_regionManager.RegisterViewWithRegion("MenuRegion", () => _container.Resolve<ClassesButton>());
On the _container.Resolve() I get AG_E_PARSER_BAD_PROPERTY_VALUE for "ContainerCommand ClassesCommand()" in the XAML.
My ClassesCommand.cs is :
public class ClassesCommand
{
public void Execute()
{
//
}
public bool CanExecute()
{
//
return true;
}
}
JD.
Try registering your command by Key instead of type. Also, try removing the empty parenthesis from the end. Let me know if either of these things fixes your issue. Thanks!