I'm working on a WPF application that's utilizing the Microsoft Prism framework. One aspect of the application is a "modal" region that can hold any number of modal windows that overlays over the entire window. As more views are navigated into the region, each window slides to the right to allow the new window to occupy the center of the screen. Here's a more visual explanation:
When the "modal" region contains a single view:
When another view is added to the region:
When several more views are added:
I have this working using a custom control that manages the animation and display of its children. Here's what the control's custom RegionAdapter's Adapt method looks like:
protected override void Adapt(IRegion region, ModalContainer regionTarget)
{
region.ActiveViews.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler((o, e) =>
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach(FrameworkElement element in e.NewItems)
{
regionTarget.AddChild(element);
}
}
else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
foreach (FrameworkElement element in e.OldItems)
{
regionTarget.RemoveChild(element);
}
}
});
}
My question is this: What's the best way to navigate back to an earlier window? Right now, the only way I know to trigger the RemoveChild method above is to explicitly remove the view from the region, which requires that I keep a list of all the views currently in the region somewhere:
// to remove the most recently added view from the region
_regionManager.Regions["ModalRegion"].Remove(addedViews.Pop());
Ideally, I would be able to navigate backwards using Prism's "journaling" concept, but I don't see a way in my RegionAdapter to respond to when a view already in the region is re-navigated to.
Any hints would be much appreciated.
EDIT
I was able to achieve this functionality by following a suggestion by GOstrowsky (see the comments in the accepted answer) - I changed my region adapter to only maintain a single active view in the region (the view currently in the center of the screen). I then can target that view for removal via myRegion.ActiveViews.FirstOrDefault().
YET ANOTHER EDIT
I have since changed this implementation yet again, as we needed the ability to remove any of the views currently in the region, not just the last one. See the accepted answer for details.
If you would ocassionally want to Navigate back to a previous View, you should not remove it from the Region when Navigating from it.
Instead, you could just deactivate it from the OnNavigatedFrom() method. And then, use the NavigationJournal to navigate back.
Regarding your RegionAdapter control you could modify it so that it could handle View activation and de-activation. For example, you could Publish a De/ActivationChanged event from each ViewModel OnNavigatedFrom() and OnNavigatedTo(), and handle these events on your custom control by Subscribing to it and performing the corresponding task to each event.
You can find more information about Navigation and Event Aggregation in the following MSDN Prism Guide chapters:
8: Navigation
Event Aggregation
I hope this helps,
Regards.
Initially, I solved this problem by only allowing a single region to be active, which guaranteed that removing the region's currently active view always removed the view currently in the center of the screen. However, since then, we've needed the ability to remove any of the views from the region, not just the first. To accomplish this, I realized that the Region.Views property can be cast to a List and then accessed by index:
List<object> allViews = modalRegion.Views.ToList<object>();
I'm a little uncomfortable with this solution, since the IViewsCollection definition inherits from IEnumerable, not IList; technically I could be handed a custom IViewsCollection that cannot be cast to an IList.... but in the short term I'm going to run with this.
Related
I'm trying to find solution for the following problem. I have a WPF app, I used mvvm and prism (most recent version 7) to build it. Here is the draft of the form/dialog I work on:
MainView has region - region1, I inject SubViewA into region1 based on what is selected in treeview. This view represents treeitem content. SubViewA itslef has region - region2, and another view - SubViewB is injected into region2 based on combobox selection.
I use INavigationAware to manage injection to the region.
So to inject view into region I use from MainViewModel the following:
_regionManager.RequestNavigate(regionName, viewName, callBack, parameters);
In the SubViewAViewModel I implement INavigationAware, and to reuse created views I check if view per treeitem was created. To do it I add into parameters a treeitemId and then I check this id in IsNavigationTarget method like this:
bool IsNavigationTarget(NavigationContext navigationContext)
{
// get id parameter from navigationContext.Parameters
// check if subviewA for treeitemId was already shown and return true,
// i use dictionary, where i store ids of all items that were selected in the past
// otherwise return false.
}
The same method I use when I want to inject SubViewB into region 2 of SubViewA. Mostly when user changes dropdown selection new SubViewB is injected.
Now my question is - if I use INavigationAware in SubViewBViewModel and when IsNavigationTarget always returns true - all is good. When I try to reuse views and again chose what to return true or false, then when I select second item in treeview I got an exception: "Region with the given name is already registered" - prism complains that region2 was already registered.
I know that I can have service and always populate data from the service when View is shown, and because of that I don't need to reuse views. But it's more academic question - what is the proper way to solve it?
P.S. I tried to register scoped region manager, but I was not successful, my problem is I don't know where is the best place to create new scoped manager and how to inject it into viewmodel. Parent's ViewModel is not good place to do it, because I have to expose view. If I try to use attached behavior, then it seems, region content is updated before behavior is invoked.
P.S.2 I found an example from pluralsight (by Brian Lagunas), where he creates two shells, but it differs from what I want to achieve. He creates new scope manager per window in the moment when window is created. And also if window itself will have the same structure as I showed above, it will fail too.
P.S.3 I watched recent streams from Brian Lagunas where he is writing outlook from scratch, his approach is based on behavior, he associates some view with dependent views, it works fine, but again in his example dependent views don't contain regions.
Thank you.
For those who are interested in details, you have to watch the following pluralsight courses: pluralsight.pxf.io/XVxR5 & pluralsight.pxf.io/B6X99. One is about multiple shells and another is about tabbed control, which is called 'Prism Problems & Solutions: Mastering TabControl' - this course helped me.
In short, the problem is about how to inject scope region in the main viewmodel. To solve it we have override ScopedRegionNavigationContentLoader plus to control either we want to inject scoped region manager or global one we have to us marking interfaces.
I created another question which is similar to current one: please check Prism 7 throws and exception when working with nested views. It will give you more details.
I'm very new to WPF. I want to create a dual monitor/projector application. What I want to do is have the "presenters screen" on one monitor and another panel on the secondary monitor, similar to how powerpoint works. I'm struggling to wrap my mind around the panels and XAML. So what I'm after is user clicks on a button on screen1 and information gets updated on screen2.
I'm using this code:
this.Width = System.Windows.SystemParameters.VirtualScreenWidth;
this.Height = System.Windows.SystemParameters.VirtualScreenHeight;
this.Top = 0;
this.Left = 0;
to set the width and height of the screen.
Edit:
The later goal is to cause screen2 to retrieve items out of a database based on the selection on screen1
Question: tutorials, places to go, nudges on how to update monitor2 from a button on monitor1
Short Answer
Create a view model that shared between two views; make one of the views the master (makes the changes) and the other pure presentation. The views are new windows. Initially do not be concerned with the window position (we'll get to that later) just get the shared viewmodel working.
Tip: research the MVVM pattern. Google has a lot of articles on the subject.
Long Answer
After you have researched MVVM and created a few example applications (from scatch or using a framework), below are few additional features you want implement to create the "powerpoint-like" application.
Fullscreen Mode
At the very least you will want the presentation window to be full screen. To achieve this, you set the WindowStyle to None and AllowsTransparency to True.
If you want to make the second window also fullscreen you may need to do some Win32 overrides to get the window to maximize properly without covering the taskbar (post a comment if you want to know how to do this).
Detect Multiple Monitors
Get the size and position of the monitors using Win32 Interop commands. There will be plenty of articles on the Internet that will help you with this (or post another StackoverFlow question).
This would be a neatâ„¢ feature as it will position the two windows correctly (use the secondary screen as the presentation).
That is all that I can think of now, post-back if you any questions on MVVM or any of the additional points above.
1) You should have 2 Windows, the way this looks I'd make monitor2 a child window of monitor1 (after all, it is a child ;)
What i mean by that, is that StartupUri in App.xaml should point to monitor1, and in monitor1's constructor, you should create an instance of monitor2 (which would be a singleton if i were to do it).
2) To maximize a window on the second screen:
Subscribe to the Loaded event of the window (in code-behind), and set
private void Window_Loaded(object sender, RoutedEventArgs e) {
WindowState = WindowState.Maximized;
}
More info (and source): here
3) As to how to make monitor2 react when you set something in monitor1, make monitor1 and monitor2 bind to the same ViewModel, only they show different stuff.
Hope this helps!
I have a Region which can have only one active view at a time. I want to add new view to the region on user action and remove the existing view from the same region. I also want to maintain the cache of few views. If the no of view is more than the specified limit then I will remove the oldest view. Is there any direct support for it or I have to implement the Region adapter for it. Is there any other better approach for the same?
Well, let me answer your two questions.
First, if you want a Region to only show one view (like say you have a region defined as a ContentControl), that is possible. You can add many views to that region and only the active one will be shown. To show a different view in that region that has already been added, you would simply Activate that view:
var region = regionManager.Regions["TabRegion"];
region.Add(view1);
region.Add(view2);
region.Activate(view2);
In this way, you can have many instantiated views ready to go, but only one visible.
Second, with the expirations. I'd say a region adapter would be the cleanest and most correct way, but you could just create an expiring cache for these and when they expire, you can remove them from the region if they aren't active:
var region = regionManager.Regions["TabRegion"];
region.Add(view1);
regionTracker.Add(view1, region, TimeSpan.FromMinutes(10));
region.Add(view2);
regionTracker.Add(view2, region, TimeSpan.FromMinutes(10));
region.Activate(view2);
And then the implementation of your expiration for your regionTracker could just:
if(!region.ActiveViews.Contains(ViewThatJustExpired))
{
region.Remove(ViewThatJustExpired);
}
It's a bit half-baked, but hopefully this will give you some idea of where to go.
Take a look at my blog post about dynamic module loading in PRISM with Navigation. In that post you'll see how I use a multiple-view container, then swap views into and out of focus. It involves having an interface for navigation, then raising events that swap the view state using the visual state manager.
Click Here to View
Jeremy
I'm starting a new project in Wpf, and am now looking into using Prism. For now I'm simply trying to set up the navigation of the application using Prism. Unfortunately my lack of experience with the framework makes it a bit difficult to get started..
To be more precise about my first challenge I have an application with a "navigation/menu" region and a "main" region. The navigation region will be the same for all different main region views, and I therefore define the menu in the shell.xaml. When clicking any menu item I'd like to add a view to the region using Prism. First; is this something one typically will use Prism for? If so; what's the typical approach? And I mean on a more structural level..
My impression is that Prism will make my application much more scalable in the end, and I see that I get some other advantages from it - like the IoC container. So I would like to use it - if I could only get through the first steps..
I've got a sample that uses a little more of the CAG feel for modules contributing to a menu and how to add views to a region. It ought to make things a little more clear.
http://dl.getdropbox.com/u/376992/CAGMenus.zip
Hope this helps,
Anderson
Bumbuska,
Prism will be a great way for you to achieve this functionality and it is pretty easy to do once you understand the principals.
The way I will do it is to add event listeners in the start up event of your Main Region. When you select your item in the menu, you fire the event. When that happens your Main Region will handle the event and you clear the current view from the Main Region. Then create the new view you want to use and add it.
Your main region should look something like this:
public void Initialize()
{
Events.PageEvents.ClickedEvent1 ce1 = this.eventAggregator.GetEvent<Events.PageEvents.ClickedEvent1>();
ce1.Subscribe(LoadView, ThreadOption.UIThread, true);
}
private void LoadView(Events.HomePageEvents.Clicked clicked1)
{
IRegion mainRegion = RegionManager.Regions["MainRegion"];
foreach (object view in new List<object>mainRegion.Views))
{
RegionManager.Regions["MainRegion"].Remove(view);
}
IModule firstModule = Container.Resolve<Modules.FirstModule>();
firstModuleModules.Initialize();
}
I hope that points you in the right direction. Please let me know if you need any more info.
How can I hide one panel in Visual Studio 2008 Form Designer like a layer in PS? Otherwise, can someone recommend another better method to design multiple "screens" that must be clicked through by the user?
What you describe is a wizard, and you might want to investigate the approach from Eric J.
However, when I have cases where I want to have multiple panels in the same space within my UI and I want to switch between them in the designer, I like to use a TabControl and hide the tabs on the TabControl. This makes the UI easier to manage at design time and the code is pretty simple to switch between the tabs at run time.
I made a custom control that derives from TabControl called HiddenTabsControl that is very simple. The class only overrides the WndProc and lets the TabControl base class handle everything else. All you need to do is:
Add a New Item to your project
Choose Custom Control,
Name it something like HiddenTabsControl.
Change the base Class to TabControl, remove the Constructor and the OnPaint override that Visual Studio added.
Copy this override for WndProc into the class:
protected override void WndProc(ref Message m)
{
// Hide tabs by trapping the TCM_ADJUSTRECT message
if (m.Msg == 0x1328 && !DesignMode)
{
m.Result = (IntPtr)1;
}
else
{
base.WndProc(ref m);
}
}
Now you can change tabs in the designer and design the UI easily and in the code you can handle events to change tabs as needed. Changing the Selected tab is easily done with:
this.hiddenTabsControl.SelectedTab = this.tabPageYouWantVisible;
One side effect of removing the tabs is the space that the tabs occupy when the control is constructed. Removing them will make the space the HiddenTabsControl occupies change by shrinking it. I usually set the Anchor of the HiddenTabsControl to bottom to keep it from shrinking.
I used this Wizard code in a recent project and it worked well.
It provides the basic experience you are after.
Another less elegant, but quick hack, approach is to simply not add the panel to the parent form until runtime. In doing that, the designer has no idea where the panel belongs prior to compilation, and it won't be displayed.
For example, find the block of code where you add controls to the parent form:
//this->Controls->Add(this->panel_X);
this->Controls->Add(this->tabControl);
this->Controls->Add(this->menuStrip_topMenu);
Comment or remove the statement, then find the handle to the event that occurs when the form is loaded:
this->Load += gcnew System::EventHandler(this, &MainForm::MainForm_Load);
Then in the definition of the event handler, add the control to the form:
System::Void MainForm_Load(System::Object^ sender, System::EventArgs^ e) {
...
...
this->Controls->Add(this->panel_X);
}
I haven't experienced any unwanted side effects by doing this, but if anyone has a good reason to not I'd be interested in hearing it.