I am searching for a way to create a "Yes to all - Yes - No - No to all" message box. I first tried to use a MessageBox instance but I understood that it is impossible to add custom buttons to it. So here I am with a custom window I created myself which does what I want but its design is far from being at least not ugly.
So my question is: How to reproduce the same layout of a MessageBox like below but with extra buttons?
There is a lot of examples of custom message boxes, both for WPF and WinForms.
I'm very sorry about amount of text, but here is detailed explanation and code examples.
As #mm8 suggested, the easiest way is to create simple Window, build layout for Header (Caption), Icon, Message and Buttons.
XAML:
<Window x:Class="WPFApp.CustomMessageBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFApp"
mc:Ignorable="d"
Title=""
MinHeight="150"
Width="500"
SizeToContent="Height"
ResizeMode="NoResize"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
FontSize="14"
WindowStartupLocation="CenterScreen">
<Grid Margin="5" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="64"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="*"/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<!-- Border for our custom message box -->
<Border Grid.ColumnSpan="3"
Grid.RowSpan="3"
BorderBrush="Gray"
BorderThickness="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border.Effect>
<DropShadowEffect BlurRadius="4"
ShadowDepth="0"
Direction="270"
Color="Black"
RenderingBias="Performance"/>
</Border.Effect>
</Border>
<!-- Header of our message box to keep Caption and to be used for window move -->
<TextBlock x:Name="CMBCaption"
HorizontalAlignment="Stretch"
Grid.Row="0"
Text="Custom Message Box Caption"
Grid.ColumnSpan="2"
Background="Gainsboro"
Foreground="Black"
FontWeight="SemiBold"
Margin="1,1,1,0"
Padding="5,2.5,0,0"
MouseLeftButtonDown="OnCaptionPress"/>
<!-- Icon for our custom message box -->
<Image x:Name="CMBIcon"
Grid.Column="0"
Grid.Row="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Width="36"
Height="36"/>
<!-- TextBlock for message content. Wrapped into Label because of alignment needs -->
<Label Grid.Column="1"
Grid.Row="1"
VerticalContentAlignment="Center"
Margin="2,24,4,24">
<TextBlock x:Name="CMBMessage"
TextWrapping="Wrap"
Text="Custom Message Box Message"/>
</Label>
<!-- Background for button block -->
<Rectangle Grid.Row="2"
Grid.ColumnSpan="2"
Fill="Gainsboro"
Margin="1,0,1,1"/>
<!-- Buttons block -->
<StackPanel x:Name="CMBButtons"
Grid.Row="2"
Grid.ColumnSpan="2"
Orientation="Horizontal"
FlowDirection="RightToLeft"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
Margin="0,0,6,0"/>
</Grid>
</Window>
So here is TextBlock ("CMBCaption" for our Caption), Image ("CMBIcon" for our icon), TextBlock ("CMBMessage" for our message, putted into Label as Content property to make correct alignment) and StackPanel ("CMBButtons" for some amount of buttons). "CMB" (if not obvious) is
abbreviation of CustomMessageBox.
That will give you simple little window, which can be movable (by MouseLeftButtonDown="OnCaptionPress" handler on Caption TextBlock), looks simple and fresh, is stretchable (depending on content size) and has StackPanel at bottom to store any amount of buttons you wish.
Code-behind: (check comments and remarks below)
using System.Drawing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using Brushes = System.Windows.Media.Brushes;
namespace WPFApp
{
public partial class CustomMessageBox : Window
{
// Field that will temporarily store result before we return it and close CustomMessageBox
private static CustomMessageBoxResult result = CustomMessageBoxResult.OK;
// Buttons defined as properties, because couldn't be created (initialized) with event subscription at same time "on-the-fly".
// You can add new different buttons by adding new one as property here
// and to CustomMessageBoxButtons and CustomMessageBoxResult enums
private Button OK
{
get
{
var b = GetDefaultButton();
b.Content = nameof(OK);
b.Click += delegate { result = CustomMessageBoxResult.OK; Close(); };
return b;
}
}
private Button Cancel
{
get
{
var b = GetDefaultButton();
b.Content = nameof(Cancel);
b.Click += delegate { result = CustomMessageBoxResult.Cancel; Close(); };
return b;
}
}
private Button Yes
{
get
{
var b = GetDefaultButton();
b.Content = nameof(Yes);
b.Click += delegate { result = CustomMessageBoxResult.Yes; Close(); };
return b;
}
}
private Button No
{
get
{
var b = GetDefaultButton();
b.Content = nameof(No);
b.Click += delegate { result = CustomMessageBoxResult.No; Close(); };
return b;
}
}
// Add another if you wish
// There is no empty constructor. As least "message" should be passed to this CustomMessageBox
// Also constructor is private to prevent create its instances somewhere and force to use only static Show methods
private CustomMessageBox(string message,
string caption = "",
CustomMessageBoxButtons cmbButtons = CustomMessageBoxButtons.OKOnly,
CustomMessageBoxIcon cmbIcon = CustomMessageBoxIcon.None)
{
InitializeComponent();
// Handle Ctrl+C press to copy message from CustomMessageBox
KeyDown += (sender, args) =>
{
if (Keyboard.IsKeyDown(Key.LeftCtrl) && Keyboard.IsKeyDown(Key.C))
Clipboard.SetText(CMBMessage.Text);
};
// Set message
CMBMessage.Text = message;
// Set caption
CMBCaption.Text = caption;
// Setup Buttons (depending on specified CustomMessageBoxButtons value)
// As StackPanel FlowDirection set as RightToLeft - we should add items in reverse
switch (cmbButtons)
{
case CustomMessageBoxButtons.OKOnly:
_ = CMBButtons.Children.Add(OK);
break;
case CustomMessageBoxButtons.OKCancel:
_ = CMBButtons.Children.Add(Cancel);
_ = CMBButtons.Children.Add(OK);
break;
case CustomMessageBoxButtons.YesNo:
_ = CMBButtons.Children.Add(No);
_ = CMBButtons.Children.Add(Yes);
break;
case CustomMessageBoxButtons.YesNoCancel:
_ = CMBButtons.Children.Add(Cancel);
_ = CMBButtons.Children.Add(No);
_ = CMBButtons.Children.Add(Yes);
break;
// Add another if you wish
default:
_ = CMBButtons.Children.Add(OK);
break;
}
// Set icon (depending on specified CustomMessageBoxIcon value)
// From C# 8.0 could be converted to switch-expression
switch (cmbIcon)
{
case CustomMessageBoxIcon.Information:
CMBIcon.Source = FromSystemIcon(SystemIcons.Information);
break;
case CustomMessageBoxIcon.Warning:
CMBIcon.Source = FromSystemIcon(SystemIcons.Warning);
break;
case CustomMessageBoxIcon.Question:
CMBIcon.Source = FromSystemIcon(SystemIcons.Question);
break;
case CustomMessageBoxIcon.Error:
CMBIcon.Source = FromSystemIcon(SystemIcons.Error);
break;
case CustomMessageBoxIcon.None:
default:
CMBIcon.Source = null;
break;
}
}
// Show methods create new instance of CustomMessageBox window and shows it as Dialog (blocking thread)
// Shows CustomMessageBox with specified message and default "OK" button
public static CustomMessageBoxResult Show(string message)
{
_ = new CustomMessageBox(message).ShowDialog();
return result;
}
// Shows CustomMessageBox with specified message, caption and default "OK" button
public static CustomMessageBoxResult Show(string message, string caption)
{
_ = new CustomMessageBox(message, caption).ShowDialog();
return result;
}
// Shows CustomMessageBox with specified message, caption and button(s)
public static CustomMessageBoxResult Show(string message, string caption, CustomMessageBoxButtons cmbButtons)
{
_ = new CustomMessageBox(message, caption, cmbButtons).ShowDialog();
return result;
}
// Shows CustomMessageBox with specified message, caption, button(s) and icon.
public static CustomMessageBoxResult Show(string message, string caption, CustomMessageBoxButtons cmbButtons, CustomMessageBoxIcon cmbIcon)
{
_ = new CustomMessageBox(message, caption, cmbButtons, cmbIcon).ShowDialog();
return result;
}
// Defines button(s), which should be displayed
public enum CustomMessageBoxButtons
{
// Displays only "OK" button
OKOnly,
// Displays "OK" and "Cancel" buttons
OKCancel,
// Displays "Yes" and "No" buttons
YesNo,
// Displays "Yes", "No" and "Cancel" buttons
YesNoCancel,
// Add another if you wish
}
// Defines icon, which should be displayed
public enum CustomMessageBoxIcon
{
None,
Question,
Information,
Warning,
Error
}
// Defines button, pressed by user as result
public enum CustomMessageBoxResult
{
OK,
Cancel,
Yes,
No
// Add another if you wish
}
// Returns simple Button with pre-defined properties
private static Button GetDefaultButton() => new Button
{
Width = 72,
Height = 28,
Margin = new Thickness(0, 4, 6, 4),
Background = Brushes.White,
BorderBrush = Brushes.DarkGray,
Foreground = Brushes.Black
};
// Converts system icons (like in original message box) to BitmapSource to be able to set it to Source property of Image control
private static BitmapSource FromSystemIcon(Icon icon) =>
Imaging.CreateBitmapSourceFromHIcon(icon.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
// Handler on CustomMessageBox caption-header to allow move window while left button pressed on it
private void OnCaptionPress(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
DragMove();
}
}
}
Remarks.
First of all, to simulate original MessageBox and call it only with CustomMessageBox.Show() (not with new CustomMessageBox().Show() as regular window) you should hide window constructor by making it private.
Enums CustomMessageBoxButtons, CustomMessageBoxIcon and CustomMessageBoxResult are replacements for MessageBoxButton, MessageBoxIcon and MessageBoxResult enums from original MessageBox.
Each Button stored as private property in CustomMessageBox class. On Buttons added to StackPanel on switch statement. They added in order to lay "from right to left" (so first added button will be at most right, next - left of first etc.).
GetDefaultButton method returns, as commented, simple button with pre-defined properties. You can customize it in any way, using gradients, styles, magic - whatever. Even you can remove it and set different style for each Button in its property (in private Button OK, private Button Cancel I mean) to make OK button green, Cancel button red, Yes button pink etc. GetDefaultButton may be rewrited to some kind of common GetButton if you want one method to create any button with specified text and click action as arguments:
private static Button GetButton(string buttonText, RoutedEventHandler clickAction)
{
Button button = new Button
{
Width = 72,
Height = 28
// and other
};
button.Content = buttonText;
button.Click += clickAction;
return button;
}
// In switch statement, when adding buttons to StackPanel, you create and add it instantly
private CustomMessageBox(...)
{
InitializeComponent();
// ...
switch (cmbButtons)
{
case CustomMessageBoxButtons.OKOnly:
_ = CMBButtons.Children.Add(GetButton("OK", delegate
{
result = CustomMessageBoxResult.OK;
Close();
}));
break;
// ...
}
}
private static BitmapSource FromSystemIcon methods, again, as commented, uses default MessageBox icon (or if correct, System Icon) to convert it to BitmapSource. It is needed, because you can't set System Icon to default WPF Image control as Source. If you want to use own icons/images you can remove it and rewrite switch statement where icons set to CMBIcon.Source with paths (URIs) to your own icons/images.
private void OnCaptionPress is handler from TextBlock which stores Caption (or is Header) of CustomMessageBox. Because in XAML WindowStyle property setted to "None" - we can't move window as it is borderless, so this handler uses header to move window until left mouse button pressed on it.
This CustomMessageBox doesn't have Close (x) button. I don't add it because each dialog button has Close() call at click action. You can add if you wish, but this have no sense.
I set window Width property to 500px, so it stretches only by heigth. You also can change it, even make resizable or return WindowStyle to default value (with minimize, maximize and close buttons).
I've also added different "Caption" TextBlock background color in switch statement where icon sets to make MessageBox be better perceived "by eye", but code is huge enough so I removed it from example.
And finally.
To add your Yes to all, No to all or other buttons, on this example, I placed comments // Add another if you wish in places, where you should add new ones:
Add YesToAll, NoToAll or other entries to CustomMessageBoxButtons enum to be able specify it when call, for example, as CustomMessageBox.Show(..., ..., CustomMessageBoxButtons.YesToAllNoToAll).
Add also entries to CustomMessageBoxResult enum to be able return them as result.
Add new Button property, named YesToAll, NoToAll or anyway you need. Set a .Content property with text, that should be displayed on this button ("Yes to all", "No to all" etc.). Add .Click handler, in which set result field with CustomMessageBoxResult enum value of this button and put Close() method to call window close.
And usage is simple, as original MessageBox:
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var result1 = CustomMessageBox.Show("Some message");
var result2 = CustomMessageBox.Show("Some message", "My caption");
var result3 = CustomMessageBox.Show("Some message", "My caption", CustomMessageBoxButtons.OKOnly);
var result4 = CustomMessageBox.Show("Some message", "My caption", CustomMessageBoxButtons.OKCancel, CustomMessageBoxIcon.Warning);
// Do whatever with result
}
Few examples:
Only message
Message + Caption
Message + Caption + Buttons
Message + Caption + Buttons + Icon
I specially removed Yes to all, No to all button from example to allow you try create it by yourself. You have, i think, detailed guidance now.
I am using WPF Live-Charts (https://lvcharts.net)
I want the tooltip to display the point value according to the mouse cursor movement, as in the image link below.
I tried, but I haven't found a way to display the tooltip without hovering the mouse cursor over the point in Live-Charts.
Examples:
If anyone has done this, can you give some advice?
The solution is relatively simple. The problem with LiveCharts is, that it not well documented. It gets you easily started by providing some examples that target general requirements. But for advanced scenarios, the default controls doesn't offer enough flexibility to customize the behavior or layout. There is no documentation about the details on how things work or what the classes of the library are intended for.
Once I checked the implementation details, I found the controls to be really horrible authored or designed.
Anyway, this simple feature you are requesting is a good example for the shortcomings of the library - extensibility is really bad. Even customization is bad. I wish the authors would have allowed templates, as this would make customization a lot easier. It should be simple to to extend the existing behavior, but apparently its not, unless you know about undocumented implementation details.
The library doesn't come in like a true WPF library. I don't know the history, maybe it's a WinForms port by WinForms devs.
But it's free and open source. And that's a big plus.
The following example draws a cursor on the plotting area which snaps to the nearest chart point and higlights it, while the mouse is moving.
A custom ToolTip follows the mouse pointer to show info about the currently selected chart point:
ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
var chartValues = new ChartValues<Point>();
// Create a sine
for (double x = 0; x < 361; x++)
{
var point = new Point() {X = x, Y = Math.Sin(x * Math.PI / 180)};
chartValues.Add(point);
}
SeriesCollection = new SeriesCollection
{
new LineSeries
{
Configuration = new CartesianMapper<Point>()
.X(point => point.X)
.Y(point => point.Y),
Title = "Series X",
Values = chartValues,
Fill = Brushes.DarkRed
}
};
}
private ChartPoint selectedChartPoint;
public ChartPoint SelectedChartPoint
{
get => this.selectedChartPoint;
set
{
this.selectedChartPoint = value;
OnPropertyChanged();
}
}
private double cursorScreenPosition;
public double CursorScreenPosition
{
get => this.cursorScreenPosition;
set
{
this.cursorScreenPosition = value;
OnPropertyChanged();
}
}
public SeriesCollection SeriesCollection { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
private void MoveChartCursorAndToolTip_OnMouseMove(object sender, MouseEventArgs e)
{
var chart = sender as CartesianChart;
if (!TryFindVisualChildElement(chart, out Canvas outerCanvas) ||
!TryFindVisualChildElement(outerCanvas, out Canvas graphPlottingArea))
{
return;
}
var viewModel = this.DataContext as ViewModel;
Point chartMousePosition = e.GetPosition(chart);
// Remove visual hover feedback for previous point
viewModel.SelectedChartPoint?.View.OnHoverLeave(viewModel.SelectedChartPoint);
// Find current selected chart point for the first x-axis
Point chartPoint = chart.ConvertToChartValues(chartMousePosition);
viewModel.SelectedChartPoint = chart.Series[0].ClosestPointTo(chartPoint.X, AxisOrientation.X);
// Show visual hover feedback for previous point
viewModel.SelectedChartPoint.View.OnHover(viewModel.SelectedChartPoint);
// Add the cursor for the x-axis.
// Since Chart internally reverses the screen coordinates
// to match chart's coordinate system
// and this coordinate system orientation applies also to Chart.VisualElements,
// the UIElements like Popup and Line are added directly to the plotting canvas.
if (chart.TryFindResource("CursorX") is Line cursorX
&& !graphPlottingArea.Children.Contains(cursorX))
{
graphPlottingArea.Children.Add(cursorX);
}
if (!(chart.TryFindResource("CursorXToolTip") is FrameworkElement cursorXToolTip))
{
return;
}
// Add the cursor for the x-axis.
// Since Chart internally reverses the screen coordinates
// to match chart's coordinate system
// and this coordinate system orientation applies also to Chart.VisualElements,
// the UIElements like Popup and Line are added directly to the plotting canvas.
if (!graphPlottingArea.Children.Contains(cursorXToolTip))
{
graphPlottingArea.Children.Add(cursorXToolTip);
}
// Position the ToolTip
Point canvasMousePosition = e.GetPosition(graphPlottingArea);
Canvas.SetLeft(cursorXToolTip, canvasMousePosition.X - cursorXToolTip.ActualWidth);
Canvas.SetTop(cursorXToolTip, canvasMousePosition.Y);
}
// Helper method to traverse the visual tree of an element
private bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild resultElement)
where TChild : DependencyObject
{
resultElement = null;
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is Popup popup)
{
childElement = popup.Child;
}
if (childElement is TChild)
{
resultElement = childElement as TChild;
return true;
}
if (TryFindVisualChildElement(childElement, out resultElement))
{
return true;
}
}
return false;
}
}
MainWindow.xaml
<Window>
<Window.DataComtext>
<ViewModel />
</Window.DataContext>
<CartesianChart MouseMove="MoveChartCursorAndToolTip_OnMouseMove"
Series="{Binding SeriesCollection}"
Zoom="X"
Height="600">
<CartesianChart.Resources>
<!-- The cursor for the x-axis that snaps to the nearest chart point -->
<Line x:Key="CursorX"
Canvas.ZIndex="2"
Canvas.Left="{Binding SelectedChartPoint.ChartLocation.X}"
Y1="0"
Y2="{Binding ElementName=CartesianChart, Path=ActualHeight}"
Stroke="Gray"
StrokeThickness="1" />
<!-- The ToolTip that follows the mouse pointer-->
<Border x:Key="CursorXToolTip"
Canvas.ZIndex="3"
Background="LightGray"
Padding="8"
CornerRadius="8">
<StackPanel Background="LightGray">
<StackPanel Orientation="Horizontal">
<Path Height="20" Width="20"
Stretch="UniformToFill"
Data="{Binding SelectedChartPoint.SeriesView.(Series.PointGeometry)}"
Fill="{Binding SelectedChartPoint.SeriesView.(Series.Fill)}"
Stroke="{Binding SelectedChartPoint.SeriesView.(Series.Stroke)}"
StrokeThickness="{Binding SelectedChartPoint.SeriesView.(Series.StrokeThickness)}" />
<TextBlock Text="{Binding SelectedChartPoint.SeriesView.(Series.Title)}"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding SelectedChartPoint.X, StringFormat=X:{0}}" />
<TextBlock Text="{Binding SelectedChartPoint.Y, StringFormat=Y:{0}}" />
</StackPanel>
</Border>
</CartesianChart.Resources>
<CartesianChart.AxisY>
<Axis Title="Y" />
</CartesianChart.AxisY>
<CartesianChart.AxisX>
<Axis Title="X" />
</CartesianChart.AxisX>
</CartesianChart>
<Window>
I am all confused going about implementing this in Prism. My scenario in one liner is how to achieve Prism Navigation (regionManager.RequestNavigate) in a view that is shown as a separate modal/non modal window over the main window.
Taking some code from this article, I am now able to show a separate Window, but I am very confused about navigating in the regions of the window shown. I will try to put up some code below to clarify my situation.
This code in RoomBandViewModel launches dialog
private void ManageRoomFacility() {
dialogService.ShowDialog<RoomFacilityMainWindowView>(this, container.Resolve<RoomFacilityMainWindowView>());
regionManager.RequestNavigate(RegionNames.Main_Region, new Uri("RoomFacilityMainView", UriKind.Relative));
As can be seen, I launch the Dialog which shows the View (code shown below), and then tries to navigate in One of the region of the View
The popup window RoomFacilityMainWindowView
<Window x:Class="HotelReservation.Main.View.RoomFacilities.RoomFacilityMainWindowView"
<view:RoomFacilityMainView
prism:RegionManager.RegionName="{x:Static const:RegionNames.Window_Main_Region}"/>
</Window>
UserControl within window (RoomFacilityMainView)
<UserControl x:Class="HotelReservation.Main.View.RoomFacilities.RoomFacilityMainView"
<Grid VerticalAlignment="Stretch" >
...
<Border Grid.Column="0" Style="{StaticResource RegionBorderStyle}">
<StackPanel>
<TextBlock Text="Some Sample Text"/>
<ContentControl prism:RegionManager.RegionName="{x:Static const:RegionNames.Window_List_Region}"
/>
</StackPanel>
</Border>
<GridSplitter Width="5" Grid.Column="1" HorizontalAlignment="Stretch" />
<Border Grid.Column="2" Style="{StaticResource RegionBorderStyle}" >
<TabControl x:Name="Items" Margin="5" prism:RegionManager.RegionName="{x:Static const:RegionNames.Window_Edit_Region}" />
</Border>
</Grid>
</UserControl>
Code Behind (RoomFacilityMainView.xaml.cs)
public partial class RoomFacilityMainView : UserControl {
public RoomFacilityMainView() {
InitializeComponent();
RoomFacilityMainViewModel viewModel = this.DataContext as RoomFacilityMainViewModel;
if (viewModel == null) {
viewModel = ServiceLocator.Current.GetInstance<RoomFacilityMainViewModel>();
this.DataContext = viewModel;
}
}
}
RoomFacilityMainViewModel
public class RoomFacilityMainViewModel : BindableBase {
IRegionManager regionManager;
IUnityContainer container;
public RoomFacilityMainViewModel(IRegionManager regionManager, IUnityContainer container) {
this.regionManager = regionManager;
this.container = container;
regionManager.RequestNavigate(RegionNames.Window_List_Region, new Uri("RoomFacilityListView", UriKind.Relative));
}
}
With this code no navigation occurs and I just get a blank window. The Contents of the RoomFacilityListView.xaml should be displayed, but its blank.
If the code is confusing, then please just give advice on how to navigate (use RequestNavigate) with View that has regions but shown through Dialog Service as a separate window instead of on MainWindow(Shell) .
If you're using an IDialogService implementation that shows a new window via Window.ShowDialog() method, then there's is no surprise that your navigation doesn't work. The ShowDialog() method returns only on closing the window, so your navigation request will actually be processed on a closed window, in particular after the window has closed.
There is nothing special in modal windows that would prevent using Prism regions and navigation in them. One limitation is that you cannot create multiple window instances "as is", since they all would have regions with same names, and that's not possible using one region manager. However, there is a solution: scoped region managers.
Assuming you're not going to create multiple instances, here is an example how could you solve your issue.
First, you have to ensure that your modal dialog's RegionManager is the same instance as your main RegionManager (I'm using MEF here, but actually it doesn't matter):
[Export]
public partial class Dialog : Window
{
private readonly IRegionManager rm;
[ImportingConstructor]
public Dialog(IRegionManager rm)
{
this.InitializeComponent();
this.rm = rm;
// Don't forget to set the attached property to the instance value
RegionManager.SetRegionManager(this, this.rm);
}
}
Now, extend your dialog service implementation with a method that accepts a navigation callback:
bool? ShowDialog<T>(object ownerViewModel, object viewModel, Action initialNavigationCallback = null) where T : Window
{
Window dialog = /* your instance creation code, e.g. using container */;
dialog.Owner = FindOwnerWindow(ownerViewModel);
dialog.DataContext = viewModel;
if (initialNavigationCallback != null)
{
dialog.Loaded += (s, e) => initialNavigationCallback();
}
return dialog.ShowDialog();
}
This will provide you a possibility to display a dialog with an initial navigation request, you can call it e.g. like this:
void ManageRoomFacility() {
dialogService.ShowDialog<RoomFacilityMainWindowView>(
this,
container.Resolve<RoomFacilityMainWindowView>(),
() => regionManager.RequestNavigate(
RegionNames.Main_Region,
new Uri("RoomFacilityMainView", UriKind.Relative))
);
Alternatively, you can use the state based navigation for your task. There is a sample implementation of a Send Message modal dialog in the State-Based Navigation QuickStart.
<prism:InteractionRequestTrigger SourceObject="{Binding SendMessageRequest}">
<prism:PopupWindowAction IsModal="True">
<prism:PopupWindowAction.WindowContent>
<vs:SendMessagePopupView />
</prism: PopupWindowAction.WindowContent>
</prism:PopupWindowAction>
</prism:InteractionRequestTrigger>
My application is a topmost Window switching between multiple UserControl views. Its behavior is to close when the user clicks outside the window (more generally when it loses focus), and to show again when the user clicks on the system tray icon.
I'm having trouble getting the window to get focus when it shows up after clicking the tray icon. The problem is that the window shows up without focus and it doesn't hide when the user clicks outside. The user has to first click into the window and then click outside to trigger the Window's Deactivated event.
I can reproduce the problem with the most basic example from the documentation. I'm showing below the most basic representation of the problem I could produce.
I have tried many different things, none of which have shown any different behavior. For example, I tried calling the view's Focus() in the OnViewLoaded handler in the view models and deactivating the viewmodels instead of closing the window in the Close action. I also tried this suggestion on what seemed to be the same problem.
Any hint or help as to how to do this would be greatly appreciated.
[Export(typeof(ShellViewModel))]
public class ShellViewModel : Conductor<object>
{
IWindowManager windowManager;
[ImportingConstructor]
public ShellViewModel(IWindowManager windowManager)
{
this.windowManager = windowManager;
ShowPageOne();
}
public void ShowPageOne()
{
ActivateItem(new PageOneViewModel());
}
public void ShowPageTwo()
{
ActivateItem(new PageTwoViewModel());
}
public void Close()
{
this.TryClose();
// Using this to simulate the user clicking on a system tray icon
var timer = new Timer();
timer.Tick += (s, e) =>
{
windowManager.ShowWindow(this);
timer.Stop();
};
timer.Interval = 1000;
timer.Start();
}
}
My ShellView is:
<Window x:Class="PopupTest.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tc="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
Width="300" Height="400"
cal:Message.Attach="[Event Deactivated] = [Action Close]"
Topmost="True">
<StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="ShowPageOne" Content="Show Page One" />
<Button x:Name="ShowPageTwo" Content="Show Page Two" />
</StackPanel>
<ContentControl x:Name="ActiveItem" />
</StackPanel>
My two view models are:
public class PageOneViewModel : Caliburn.Micro.Screen { }
public class PageTwoViewModel : Caliburn.Micro.Screen { }
And the views are:
<UserControl x:Class="PopupTest.PageOneView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBlock x:Name="bob" FontSize="32">Page One</TextBlock>
</UserControl>
I need to know when a ListBox has finished rendering for the first time so that I can scroll it to the top to present the user with the first item on the list.
I have a ListBox that uses RichTextBox in it's DataTemplate:
<DataTemplate x:Key="HelpTextTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
...
<ContentControl>
...
<RichTextBox x:Name="HelpTextContent" Grid.Row="1"
Tag="{Binding Path=HelpObject.Text, Mode=TwoWay}"
TextWrapping="Wrap"
HorizontalAlignment="Stretch"
Margin="0,0,20,0"
Loaded="RichTextBox_Loaded"
ContentChanged="RichTextBox_ContentChanged"
SelectionChanged="RichTextBox_SelectionChanged"/>
...
</ContentControl>
...
</Grid>
</DataTemplate>
The ListBox is bound to an ObservableCollection.
I had a problem with the scrolling of the ListBox - if height of the RichTextBox was greater than that of the ListBox the user couldn't scroll to the bottom of the RichTextBox. The ListBox would jump to the next item in the list. The height of the scroll bar's slider would change as well. This is because the actual height of the RichTextBox is only calculated when it's actually rendered. When it's off the screen the height reverts to smaller value (I think the code assumes that the text can all fit on a single line rather than having to be wrapped).
I tracked these problems down to the ListBox's use of a VirtualisingStackPanel to draw the items. When I replaced that with a simple StackPanel those problems went away.
This then created the problem I have now which is that the ListBox scrolls to the bottom of the list on initial load. The Loaded and LayoutUpdated events on the ListBox occur before the data has been loaded. I tried listening out for the PropertyChanged event on the view model when the ObservableCollection is initialised:
void editViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "ListDataSource":
// Try to scroll to the top of the ListBox
break;
}
}
this fires too early as well. The list is rendered after this event is fired and causes the ListBox to scroll to the bottom.
Try to scroll in on Loaded handler, but delay it a bit by Dispatcher. Something like that
void OnLoaded(...)
{
Dispatcher.BeginInvoke(() => {/*Scroll your ListBox here*/});
}
It could help.
In the end I had to use a kludge:
private System.Threading.Timer timer;
void editViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "ListDataSource":
TimerCallback callback = TimerResult;
timer = new Timer(callback, null, 750, 0);
break;
}
}
private void TimerResult(Object stateInfo)
{
Dispatcher.BeginInvoke(() =>
{
if (this.ItemsList.Items.Count > 0)
{
this.ItemsList.UpdateLayout();
this.ItemsList.SelectedIndex = 0;
this.ItemsList.ScrollIntoView(this.ItemsList.Items[0]);
}
});
timer.Dispose();
}
If anyone knows a better way please post your answer now.
public class AutoScrollBehavior : Behavior<ListBox>
{
#region Properties
public object ItemToScroll
{
get { return GetValue(ItemToScrollProperty); }
set { SetValue(ItemToScrollProperty, value); }
}
public static readonly DependencyProperty ItemToScrollProperty = DependencyProperty.Register("ItemToScroll", typeof(object), typeof(AutoScrollBehavior), new PropertyMetadata(ItemToScrollPropertyChanged));
private static void ItemToScrollPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ListBox lb = ((AutoScrollBehavior)d).AssociatedObject;
lb.UpdateLayout();
((AutoScrollBehavior)d).AssociatedObject.ScrollIntoView(e.NewValue);
}
#endregion
}
then use in the XAML as
<ListBox SelectedItem="{Binding SelectedItemFromModel, Mode=TwoWay}">
<i:Interaction.Behaviors>
<Behaviors:AutoScrollBehavior ItemToScroll="{Binding SelectedItemFromModel}"/>
</i:Interaction.Behaviors>
</ListBox>
on some command you should be able to control and set SelectedItemFromModel property of your view model.