I read that a few people were having a problem with this so I wanted to post a (somewhat) elegant solution I came up with while trying to deal with this. The problem is when you create templated pages in Silverlight and the ContentControls do not have the parent Frame's NavigationService (it's always null when you try and use it). There are similar scenarios where a NavigationService is present in intellisence but is always null. To enable site-wide Navigation:
Create a new UserControl (I called mine 'NavFrame') that has a Navigation Frame in it (I called mine 'RootFrame').
Inside this Frame you can set whatever content you like.
Set this UserControl as your RootVisual in App.xaml.cs (i.e. this.RootVisual = new NavFrame();).
To use the NavigationService in any of your pages you can type something like:
((NavFrame)App.Current.RootVisual).RootFrame.NavigationService
.Navigate(new Uri("Your Uri", UriKind.RelativeOrAbsolute));
You can create an Action and drag it on top of the control you want to make navigation happen, just like this one:
public class NavigateAction : TriggerAction<DependencyObject>
{
public Uri Uri
{
get;
set;
}
protected override void Invoke(object parameter)
{
var frame = FindContainingFrame(AssociatedObject);
if(frame == null)
throw new InvalidOperationException("Could not find the containing Frame in the visual tree.");
frame.Navigate(Uri);
}
protected static Frame FindContainingFrame(DependencyObject associatedObject)
{
var current = associatedObject;
while(!(current is Frame))
{
current = VisualTreeHelper.GetParent(current);
if(current == null)
return null;
}
return (Frame)current;
}
}
Now you just have to drag it and wire it to your target page. BTW this is true for SL4, never tried it on SL3. and the URI does work in the form: "/SilverlightApplication1;component/Page1.xaml" or with UriMapping on the Frame.
((Frame)(Application.Current.RootVisual as MainPage).FindName("ContentFrame"))
.Navigate(new Uri("Page Name", UriKind.Relative));
Related
I'm creating a WPF MVVM app using Caliburn Micro. I have a set of buttons in a menu (Ribbon) that live in the view for my shell view model, which is a ScreenConductor. Based on the currently active Screen view model, I would like to have the ribbon buttons be disabled/enabled if they are available for use with the active Screen, and call actions or commands on the active Screen.
This seems like a common scenario. Is there a pattern for creating this behavior?
Why don't you do the reverse thing, instead of checking which commands are supported by the current active screen, let the active screen populate the menu or ribbon tab with all the controls that it supports, (i would let it inject its own user control which might just be a complete menu or a ribbon tab all by itself), this will also enhance the user experience as it will only show the user the controls that he can work with for the current active screen.
EDIT: Just looking at your question again and I'm thinking that this is much simpler than it looks
The only issue I can see you having is that a lack of a handler (and guard) method on a child VM will mean that buttons that don't have an implementation on the currently active VM will still be enabled.
The default strategy for CM is to try and find a matching method name (after parsing the action text) and if one is not found, to leave the button alone. If you were to customise that behaviour so that the default is for buttons to be disabled, you could easily get it working by just implementing the command buttons in your shell, making sure to set the command target to the active item:
In the shell define your buttons, making sure they have a target that points to the active child VM
<Button cal:Message.Attach="Command1" cal:Action.TargetWithoutContext="{Binding ActiveItem}" />
Then just implement the method in your child VM as per usual
public void Command1() { }
and optionally a CanXX guard
public bool CanCommand1
{
get
{
if(someCondition) return false;
return true;
}
}
Assuming you don't get much more complex than this, it should work for you
I'm going to have a quick look at the CM source and see if I can come up with something that works for this
EDIT:
Ok you can customise the ActionMessage.ApplyAvailabilityEffect func to get the effect you want - in your bootstrapper.Configure() method (or somewhere at startup) use:
ActionMessage.ApplyAvailabilityEffect = context =>
{
var source = context.Source;
if (ConventionManager.HasBinding(source, UIElement.IsEnabledProperty))
{
return source.IsEnabled;
}
if (context.CanExecute != null)
{
source.IsEnabled = context.CanExecute();
}
// Added these 3 lines to get the effect you want
else if (context.Target == null)
{
source.IsEnabled = false;
}
// EDIT: Bugfix - need this to ensure the button is activated if it has a target but no guard
else
{
source.IsEnabled = true;
}
return source.IsEnabled;
};
This seems to work for me - there is no target for methods which couldn't be bound to a command, so in that case I just set IsEnabled to false. This activates buttons only when a method with a matching signature is found on the active child VM - obviously give it a good test before you use it :)
Create methods and accompanying boolean properties for each of your commands on your shell view model. (See code below for an example.) Caliburn.Micro's conventions will wire them up to the buttons for you automatically. Then simply raise property changed events for the boolean properties when you change views to have them be re-evaluated.
For example, let's say you have a Save button. The name of that button in your xaml would be Save, and in your view model, you would have a Save method along with a CanSave boolean property. See below:
public void Save()
{
var viewModelWithSave = ActiveItem as ISave;
if (viewModelWithSave != null) viewModelWithSave.Save();
}
public bool CanSave { get { return ActivateItem is ISave; } }
Then, in your conductor, whenever you change your active screen, you would call NotifyOfPropertyChange(() => CanSave);. Doing this will cause your button to be disabled or enabled depending upon if the active screen is capable of dealing with that command. In this example, if the active screen doesn't implement ISave, then the Save button would be disabled.
I would use the Caliburn.Micro event aggregation in this scenario, as follows:
Create a class named ScreenCapabilities with a bunch of Boolean attributes (e.g. CanSave, CanLoad, etc.)
Create a message named ScreenActivatedMessage with a property of type ScreenCapabilities
Create a view model for your ribbon that subscribes to (handles) the ScreenActivatedMessage
In the ribbon view model's Handle method, set the local CanXXX properties based on the supplied ScreenCapabilities.
It would look something like this (code typed by hand, not tested):
public class ScreenCapabilities
{
public bool CanSave { get; set; }
// ...
}
public class ScreenActivatedMessage
{
public ScreenCapabilities ScreenCapabilities { get; set; }
// ...
}
public class RibbonViewModel : PropertyChangedBase, IHandle<ScreenActivatedMessage>
{
private bool _canSave;
public bool CanSave
{
get { return _canSave; }
set { _canSave = value; NotifyPropertyChanged(() => CanSave); }
}
// ...
public void Handle(ScreenActivatedMessage message)
{
CanSave = message.ScreenCapabilities.CanSave;
// ...
}
}
Then, somewhere appropriate, when the screen changes, publish the message. See see Caliburn.Micro wiki for more info.
Define a property (let's say ActiveScreen) for the active screen in the shell view model.
And let's assume you have properties for the each button such as DeleteButton, AddButton.
Screen is a viewmodel for the screens.
private Screen activeScreen;
public Screen ActiveScreen
{
get
{
return activeScreen;
}
set
{
activeScreen= value;
if (activeScreen.Name.equals("Screen1"))
{
this.AddButton.IsEnabled = true;
this.DeleteButton.IsEnabled = false;
}
if else (activeScreen.Name.equals("Screen2"))
{
this.AddButton.IsEnabled = true;
this.DeleteButton.IsEnabled = true;
}
NotifyPropertyChanged("ActiveScreen");
}
}
I would like to click on a button to take me to a page
, then click on a listbox item, click on a button on the new page and pass it back to the page before without creating a new URI of the first page.
**First Page**
private void btnAddExistingMember_Click(object sender, RoutedEventArgs e)
{
NavigationService.Navigate(new Uri("/ChooseMember.xaml", UriKind.Relative));
}
**Second page after choosing listbox value**
private void btnAddSelected_Click(object sender, RoutedEventArgs e)
{
Member currMember = (Member)lstMembers.SelectedItem;
string memberID = currMember.ID.ToString();
//navigate back to first page here passing memberID
}
can it be done?
Thank you
You can store the member in the App.xaml.cs file. This is common file accesssible for all files in the application.
This works like a global variable.
//App.xaml.cs
int datafield ;
//Page1xaml.cs
(App.Current as App).dataField =10;
//Page2.xaml.cs
int x = (App.Current as App).dataField
You could create a manager class that would hold the member id. This manager class could then be accessed from both your first page and ChooseMember page.
An example of a Singleton Manager class :-
public class MyManager
{
private static MyManager _instance;
public static MyManager Instance
{
get
{
if (_instance == null)
{
_instance = new MyManager();
}
return _instance;
}
}
}
I found a solution at codeproject which was quite useful to me.
While moving from the second form, save your data in PhoneApplicationService.Current.State
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
{
// Called when a page becomes the active page in a frame
base.OnNavigatedFrom(e);
// Text is param, you can define anything instead of Text
// but remember you need to further use same param.
PhoneApplicationService.Current.State["Text"] = txtboxvalue.Text;
}
Go back using same NavigationService.GoBack(); and in OnNavigatedTo method, fetch the following code.
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (PhoneApplicationService.Current.State.ContainsKey("Text"))
txtvalue.Text = (string)PhoneApplicationService.Current.State["Text"];
}
References:
MSDN: PhoneApplicationService Class
Original Solution at: How to pass values between pages in windows phone
Sounds to me like you want to set some object as the context for another page. Messaging in MVVM Light sounds like a good solution for this. Doesn't look like you're using MVVM so this may not be immediately applicable. This post pretty much lays out what I'm saying here.
Second Page
Create your SelectedObject property and make sure to call
RaisePropertyChanged(SelectedObjectPropertyName, oldValue, value, true);
The last parameter true says to broadcast this change in value to anyone listening. You'll need to wire up some other commands for listbox selected item and button click etc, but I won't go into that here since it's not directly related to your question. Selecting the Listbox item will simply set the data item for the first page like you want to accomplish. The button click can deal with the navigation.
First Page
In your view model constructor, register to receive the change broadcasted from Second Page
Messenger.Default.Register<PropertyChangedMessage<MyObject>>(this, (action) => UpdateObject(action.NewValue));
then define UpdateObject
private void UpdateObject(MyObject newObject)
{
LocalObjectProperty = newObject;
}
You can simply use
//first page
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
string value = string.Empty;
IDictionary<string, string> queryString = this.NavigationContext.QueryString;
if (queryString.ContainsKey("memberID"))
{
memberID = queryString["memberID"];
if (memberID != "-1")
//your code here
}
base.OnNavigatedTo(e);
}
//second page
private void btnAddSelected_Click(object sender, RoutedEventArgs e)
{
Member currMember = (Member)lstMembers.SelectedItem;
string memberID = currMember.ID.ToString();
string target = "/FirstPage.xaml";
target += string.Format("?memberID={0}", memberID);
NavigationService.Navigate(new Uri(target, UriKind.Relative));
}
I 'm fairly new to Prism and I 'm currently re-writing one of our existing applications using Prism as a proof of concept project.
The application uses MVVM with a ViewModel first approach: our ViewModel is resolved by the container, and an IViewResolver service figures out what view it should be wired up to (using name conventions amongst other things).
The code (to add a view to a tab control) at the moment looks something like this:
var vm = (get ViewModel from somewhere)
IRegion reg = _regionManager.Regions["MainRegion"];
var vw = _viewResolver.FromViewModel(vm); // Spins up a view and sets its DataContext
reg.Add(vw);
reg.Activate(vw);
This all works fine, however I 'd really like to use the Prism navigation framework to do all this stuff for me so that I can do something like this:
_regionManager.RequestNavigate(
"MainRegion",
new Uri("NameOfMyViewModel", UriKind.Relative)
);
and have Prism spin up the ViewModel + View, set up the DataContext and insert the view into the region.
I 've had some success by creating DataTemplates referencing the ViewModel types, e.g.:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Module01">
<DataTemplate DataType="{x:Type local:TestViewModel}">
<local:TestView />
</DataTemplate>
</ResourceDictionary>
...and have the module add the relevant resource dictionary into the applications resources when the module is initialized, but that seems a bit rubbish.
Is there a way to effectively take over view creation from Prism, so that when RequestNavigate is called I can look at the supplied Uri and spin up the view / viewmodel based on that? There’s an overload of RegionManager.RegisterViewWithRegion that takes a delegate that allows you to supply a view yourself, and I guess I’m after something like that.
I think I might need to supply my own IRegionBehaviorFactory, but am unsure what's involved (or even if I am on the right path!).
Any help appreciated!
--
note: Originally posted over at the prism codeplex site
Sure you can do that. I 've found that Prism v4 is really extensible, if only you know where to plug in.
In this case, you want your own custom implementation of IRegionNavigationContentLoader.
Here's how to set things up in your bootstrapper (the example is from a subclass of UnityBootstrapper from one of my own projects):
protected override void ConfigureContainer()
{
// IMPORTANT: Due to the inner workings of UnityBootstrapper, accessing
// ServiceLocator.Current here will throw an exception!
// If you want access to IServiceLocator, resolve it from the container directly.
base.ConfigureContainer();
// Set up our own content loader, passing it a reference to the service locator
// (it will need this to resolve ViewModels from the container automatically)
this.Container.RegisterInstance<IRegionNavigationContentLoader>(
new ViewModelContentLoader(this.Container.Resolve<IServiceLocator>()));
}
The ViewModelContentLoader itself derives from RegionNavigationContentLoader to reuse code, and will look something like this:
public class ViewModelContentLoader : RegionNavigationContentLoader
{
private readonly IServiceLocator serviceLocator;
public ViewModelContentLoader(IServiceLocator serviceLocator)
: base(serviceLocator)
{
this.serviceLocator = serviceLocator;
}
// THIS IS CALLED WHEN A NEW VIEW NEEDS TO BE CREATED
// TO SATISFY A NAVIGATION REQUEST
protected override object CreateNewRegionItem(string candidateTargetContract)
{
// candidateTargetContract is e.g. "NameOfMyViewModel"
// Just a suggestion, plug in your own resolution code as you see fit
var viewModelType = this.GetTypeFromName(candidateTargetContract);
var viewModel = this.serviceLocator.GetInstance(viewModelType);
// get ref to viewResolver somehow -- perhaps from the container?
var view = _viewResolver.FromViewModel(vm);
return view;
}
// THIS IS CALLED TO DETERMINE IF THERE IS ANY EXISTING VIEW
// THAT CAN SATISFY A NAVIGATION REQUEST
protected override IEnumerable<object>
GetCandidatesFromRegion(IRegion region, string candidateNavigationContract)
{
if (region == null) {
throw new ArgumentNullException("region");
}
// Just a suggestion, plug in your own resolution code as you see fit
var viewModelType = this.GetTypeFromName(candidateNavigationContract);
return region.Views.Where(v =>
ViewHasDataContract((FrameworkElement)v, viewModelType) ||
string.Equals(v.GetType().Name, candidateNavigationContract, StringComparison.Ordinal) ||
string.Equals(v.GetType().FullName, candidateNavigationContract, StringComparison.Ordinal));
}
// USED IN MY IMPLEMENTATION OF GetCandidatesFromRegion
private static bool
ViewHasDataContract(FrameworkElement view, Type viewModelType)
{
var dataContextType = view.DataContext.GetType();
return viewModelType.IsInterface
? dataContextType.Implements(viewModelType)
: dataContextType == viewModelType
|| dataContextType.GetAncestors().Any(t => t == viewModelType);
}
// USED TO MAP STRINGS OF VIEWMODEL TYPE NAMES TO ACTUAL TYPES
private Type GetTypeFromName(string typeName)
{
// here you need to map the string type to a Type object, e.g.
// "NameOfMyViewModel" => typeof(NameOfMyViewModel)
return typeof(NameOfMyViewModel); // hardcoded for simplicity
}
}
To stop some confusion about "ViewModel first approach":
You use more a "controller approach", but no "ViewModel first approach". A "ViewModel first approach" is, when you inject your View in your ViewModel, but you wire up both, your ViewModel and View, through a third party component (a controller), what by the way is the (I dont want to say "best", but) most loosely coupled approach.
But to answer your Question:
A possible solution is to write an Extension for the Prism RegionManager that does exactly what you have described above:
public static class RegionManagerExtensions
{
public static void AddToRegion<TViewModel>(
this IRegionManager regionManager, string region)
{
var viewModel = ServiceLocator.Current.GetInstance<TViewModel>();
FrameworkElement view;
// Get View depending on your conventions
if (view == null) throw new NullReferenceException("View not found.");
view.DataContext = viewModel;
regionManager.AddToRegion(region, view);
regionManager.Regions[region].Activate(view);
}
}
then you can call this method like this:
regionManager.AddToRegion<IMyViewModel>("MyRegion");
I am not able to call the navigation service from the user control.
even when I create one event handler on the main page to call the navigation service that is laos not working.
Can you please help me?
I think I see the problem, but like Austin indicated, there isn't much to go on in your initial description. It sounds like you are trying to access the NavgationService (which is a PhoneApplicationPage property) from within a UserControl that you are placing on that page.
As with many things in these APIs, you have a couple of options. First, you can access the PhoneApplicationFrame (which contains your pages and manages navigation) and use it for navigation:
var frame = App.Current.RootVisual as PhoneApplicationFrame;
frame.Navigate(new Uri("/TargetPage.xaml", UriKind.Relative));
Alternatively, you can walk the control's Visual Tree using the VisualTreeHelper until you get to the containing page:
var page = GetParentOfType<PhoneApplicationPage>(this); // this is your user control
private static T GetParentOfType<T>(DependencyObject item) where T : DependencyObject
{
if (item == null) throw new ArgumentNullException("item");
T result;
var parent = VisualTreeHelper.GetParent(item);
if (parent == null) return null;
else if (parent.GetType().IsSubclassOf(typeof(T))
{
result = (T)parent;
}
else result = GetParameterOfType<T>(parent);
return result;
}
As you see, the VisualTree approach involves more code, but gets you the containing page object, where you have more access to things like NavigationContext, etc.
Hope that was your question (and your answer.)
Here's my scenario:
Shell with 1 TabControl and 1 region called MenuRegion
MenuRegion contains Buttons for each of the available modules (applications).
I want to achieve the following using Prism (Composite Application Library for WPF): When one of the buttons is clicked, I need to add a new TabItem to the TabControl, and load and individual instance of the corresponding module (application) inside this TabItem.
One module may appear several times in the TabControl.
I really appreciate your answer. But I don't believe you're using Prism (http://www.codeplex.com/CompositeWPF) are you? My question was more related to Prism, and I've edited it to be more clear now.
In Prism you dynamically load modules' views into regions. I am not sure how to do that in my scenario because the regions are to be set dynamically. How would I name them?
Thanks!
I'm new to this PRISM world (1 week experience :)) ) and had the same requirement!
First of all you have to get the Regionextensions from here.
The solution to my (may be your) problem is as follows:
have 2 regions (menu and tabcontrol - for mdi like behaviour)
tabitem header has to be prepared with a button for closing (which is bound to a command for closing this tabitem-actually hiding this tabitem)
send event from menu item to the module which should load the view (I've instantiated the modules on demand). In the module's initialize method subscribe to the event sent by the menu item. In the event handling method you simply re-show the tabitem
If this is to abstract to you I can send you a skeleton application I've developed to play around.
We do something similar, though we have the tab items already created (with no content) and show/hide as appropriate. When the tab item is selected, then we load the tab content.
private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.OriginalSource != sender) return;
TabControl tabControl = (TabControl)sender;
TabItem tabItem = (TabItem)tabControl.SelectedItem;
if (!tabItem.HasContent)
AddTabContent(tabItem); // This will cause a refresh once the content is loaded.
else
Refresh(tabItem);
}
private void AddTabContent(TabItem tabItem)
{
IOptimusPage page = tabItem.Tag as IOptimusPage;
//This allows lazy loading of controls
if (page != null)
{
if (!tabItem.HasContent)
{
CustomerEngagementUserControl control = page.GetControl(DataContext as CustomerEngagementUIObject, Services);
tabItem.Content = control;
}
}
}
The tab item content is specified in the tab item tag, using pages which are responsible for creating the content.
<TabItem
Header="Personal Background"
Style="{StaticResource FirstBreadcrumbTabItem}"
x:Name="PersonalBackgroundTab">
<TabItem.Tag>
<Pages:FfnaPersonalBackgroundPage />
</TabItem.Tag>
</TabItem>
The page creates the control.
class FfnaPersonalBackgroundPage : IOptimusPage
{
#region IOptimusPage Members
public CustomerEngagementUserControl GetControl(CustomerEngagementUIObject dataContext, CustomerEngagementServices services)
{
CustomerEngagementUserControl control = new FfnaPersonalBackgroundControl();
control.DataContext = dataContext;
control.Services = services;
return control;
}
#endregion
}
You could use a similar technique to create your tab items on the fly.
I know it's quite late a response but I am doing something similar, although haven't achieved the full solution yet.
This code happens on the click event of a button which I am handling in the presenter. The module is defined in the config file.
ModuleInfo moduleInfoObject = this.moduleEnumerator.GetModule("ModuleA");
Assembly assembly = this.LoadAssembly(moduleInfoObject);
Type type = assembly.GetType(moduleInfoObject.ModuleType);
IModule aModule = this.CreateModule(type);
aModule.Initialize();
// - - - -Helper Methods - - - -
// - - - LoadAssembly - - -
private Assembly LoadAssembly(ModuleInfo moduleInfo)
{
string assemblyFile = moduleInfo.AssemblyFile;
assemblyFile = this.GetModulePath(assemblyFile);
FileInfo file = new FileInfo(assemblyFile);
Assembly assembly;
try
{
assembly = Assembly.LoadFrom(file.FullName);
}
catch (Exception ex)
{
throw new ModuleLoadException(null, assemblyFile, ex.Message, ex);
}
return assembly;
} // LoadAssembly(moduleInfo)
// - - - CreateModule - - -
private IModule CreateModule(Type type)
{
return (IModule)containerFacade.Resolve(type);
} // CreateModule(type)
// - - - GetModulePath - - -
private string GetModulePath(string assemblyFile)
{
if (String.IsNullOrEmpty(assemblyFile))
{
throw new ArgumentNullException("assemblyFile");
} // if
if (Path.IsPathRooted(assemblyFile) == false)
{
assemblyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, assemblyFile);
} // if
return assemblyFile;
} // GetModulePath(assemblyFile)