I have a WPF window which use heavy library and takes time to be fully rendered.
This library is in an UserControl.
This window is open by a context menu command in the parent window.
Using MVVM pattern, I need to get the DialogResult of this new window when closing to access the viewmodel.
When clicking the context menu item to open this new window, the context menu stays open until the instanciation of the new window will be done.
What can I do to close the context menu before open this window?
Here is the code refactored with the help of BionicCode:
MAIN WINDOW XAML
<Image Source="{Binding ImagePath}" Height="100" Width="100">
<Image.ContextMenu>
<ContextMenu>
<MenuItem Header="Open Window"
Command="{x:Static local:MainWindow.ShowMyDialogCommand}"
CommandTarget="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget}"
/>
</ContextMenu>
</Image.ContextMenu>
</Image>
MAIN WINDOW
public partial class MainWindow : Window
{
public static RoutedCommand ShowMyDialogCommand { get; } = new RoutedCommand("ShowMyDialogCommand", typeof(MainWindow));
private readonly MainVM myMainVM;
public MainWindow()
{
InitializeComponent();
myMainVM = new MainVM();
DataContext = myMainVM;
var showMyDialogCommandBinding = new CommandBinding(ShowMyDialogCommand, ExecuteShowMyDialogCommand, CanExecuteShowMyDialogCommand);
this.CommandBindings.Add(showMyDialogCommandBinding);
}
private void CanExecuteShowMyDialogCommand(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true;
private void ExecuteShowMyDialogCommand(object sender, ExecutedRoutedEventArgs e)
{
ViewerVM vm = new ViewerVM();
var okDialog = new OkDialog()
{
Title = "Viewer Dialog",
DataContext = vm
};
bool? dialogResult = okDialog.ShowDialog();
if (dialogResult == true)
{
this.myMainVM.HandleData(vm);
}
}
}
MAIN VM
public class MainVM : ObservableObject
{
private string myImagePath;
public MainVM()
{
myImagePath = "flower.jpg";
}
public string ImagePath
{
get { return myImagePath; }
set
{
if (myImagePath == value) return;
myImagePath = value;
OnPropertyChanged(nameof(ImagePath));
}
}
public void HandleData(ViewerVM viewModel)
{
//Do stuffs
}
}
NEW WINDOW XAML
<Window.Template>
<ControlTemplate TargetType="Window">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<!-- Dynamic content row -->
<RowDefinition Height="Auto" />
<!-- Static content row (ok and cancel buttons etc.) -->
</Grid.RowDefinitions>
<!-- Dynamic content -->
<ContentPresenter Grid.Row="0" />
<!-- Static content -->
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Ok" IsDefault="True" Command="{x:Static local:OkDialog.OkCommand}" />
<Button Content="Cancel" IsCancel="True" />
</StackPanel>
</Grid>
</ControlTemplate>
</Window.Template>
NEW WINDOW
public partial class OkDialog : Window
{
public static RoutedCommand OkCommand { get; } = new RoutedCommand("OkCommand", typeof(MainWindow));
public OkDialog()
{
InitializeComponent();
var okCommandBinding = new CommandBinding(OkDialog.OkCommand, ExecuteOkCommand, CanExecuteOkCommand);
this.CommandBindings.Add(okCommandBinding);
this.DataContextChanged += OnDataContextChanged;
}
// If there is no explicit Content set, use the DataContext
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) => this.Content = e.NewValue;
private void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = (this.DataContext as IOkDialogVM).CanExecuteOkCommand() ? true : false;
private void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e)
=> this.DialogResult = true;
}
interface IOkDialogVM
{
bool CanExecuteOkCommand();
}
UserControl
<UserControl x:Class="ContextMenuTest.Viewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ddes="http://schemas.devdept.com/winfx/2008/xaml/control"
xmlns:ddgr="http://schemas.devdept.com/winfx/2008/xaml/graphics"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<ddes:Design x:Name="myDesigner" Height="300" Width="300">
<ddes:Design.Viewports>
<ddes:Viewport>
<ddes:Viewport.Background>
<ddgr:BackgroundSettings StyleMode="Solid" TopColor="White"/>
</ddes:Viewport.Background>
</ddes:Viewport>
</ddes:Design.Viewports>
</ddes:Design>
</Grid>
</UserControl>
public partial class Viewer : UserControl
{
public Viewer()
{
InitializeComponent();
}
}
public class ViewerVM : ObservableObject, IOkDialogVM
{
public bool CanExecuteOkCommand() => true;
}
App.xaml
<Application.Resources>
<DataTemplate DataType="{x:Type local:ViewerVM}">
<local:Viewer/>
</DataTemplate>
</Application.Resources>
Your current code breaks the MVVM design pattern. This is because you are managing views in your View Model. The view model class has no idea that the view will show a dialog. It therefore doesn't participate in any dialog flow.
You control the dialog completely in the View. You show it and you close it without any dependency on a view model class.
When you make use of the Button.IsCancel property the Window will close itself without the need to attach any event handler or close commands to this Button.
Setting the Window.DialogResult will always close the Window and let the Window.ShowDialog return the Window.DialogResult. You only need to attach an event handler to set the Window.DialogResult to true or false.
Window will take care of the rest. It's as easy as it can get. No View Model needed.
To show a dialog in an MVVM application, you can follow the below examples in the sections: MVVM compliant dialog flow and Advanced MVVM compliant dialog flow.
To fix the loading experience, you shouldn't create any views in the constructor. Only do some light work in the constructor so that the constructor can return fast.
As a general rule, you should always avoid creating controls in your code-behind to add them manually to the visual tree. This is done in XAML, which wouldn't cause your current issue in the first place.
If you really need to do it your way, chose to create the views either in the FrameworkElement.Loaded event or override the FrameworkElement.OnApplyTemplate method.
Because of the heavy load, I suggest to move your code to the Loaded event handler.
It's unclear what your DesignView constructor is exactly doing. In case you have shown the complete constructor and the timing of the call of the following line
devDept.LicenseManager.Unlock(typeof(devDept.Eyeshot.Workspace), "mykey");
doesn't matter or can be deferred, you should move this line to the Loaded event handler too. Just in case LicenseManager.Unlock is the blocking piece.
public partial class PartEditView : UserControl
{
private DesignerView myDesignerView;
public PartEditView()
{
InitializeComponent();
// Follow this pattern to unlock the DesignerView.
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
this.myDesignerView = new DesignerView();
this.myDesignerContainer.Children.Add(myDesignerView);
}
}
MVVM compliant dialog flow
The idea is simple, your View is responsible to show the dialog. Data is displayed/collected by binding elements to a dedicated view model of the dialog. After the dialog was closed, the View can interact with the View Model to pass over the data. In most scenarios the view model of the dialog knows how to handle the data (for example how to use the Model to persist data).
MainWindowViewModel.cs
The view model class has no idea that the view will show a dialog.
It doesn't participate in any dialog flow.
If the view model must handle the data collected by a dialog, the responsible view can pass the data to the view model.
class MainWindowViewModel : INotifyPropertyChanged
{
// Such a public method is one possible way to allow the view to pass data
// to this instance. Simply use the common means to send data from View to View Model.
public void HandleData(MyDialogViewModel viewModel)
{
}
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
public static RoutedCommand ShowMyDialogCommand { get; } = new RoutedCommand("ShowMyDialogCommand", typeof(MainWindow));
private MainWindowViewModel MainWindowViewModel { get; }
public MainWindow()
{
InitializeComponent();
this.MainWindowViewModel = new MainWindowViewModel();
this.DataContext = this.MainWindowViewModel;
var showMyDialogCommandBinding = new CommandBinding(ShowMyDialogCommand, ExecuteShowMyDialogCommand, CanExecuteShowMyDialogCommand);
this.CommandBindings.Add(showMyDialogCommandBinding);
}
private void CanExecuteShowMyDialogCommand(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true;
private void ExecuteShowMyDialogCommand(object sender, ExecutedRoutedEventArgs e)
{
var myDialogViewModel = new MyDialogViewModel();
var myDialog = new MyDialog()
{
Content = "I'm a dialog",
DataContext = myDialogViewModel
};
bool? dialogResult = myDialog.ShowDialog();
// Do something when the user has closed the dialog e.g. using the 'OK' button
if (dialogResult == true)
{
// Pass the dialog data (if it has some) to the view model class
// for further processing. The data is stored via data binding in the
// myDialogViewModel (the DataContext of the dialog).
// Depending on the context of the dialog, the dialog's view model
// knows what to do with the data (e.g. save it to a database using the Model).
this.MainWindowViewModel.HandleDialogData(myDialogViewModel);
}
}
}
MainWindow.xaml
Because the ContextMenu will have its own visual tree (it uses a Popup to display content), the routed command must be executed in the visual tree of the parent Window. For this reason we must explicitly set the MenuItem.CommandTarget property to point to the visual tree outside of the ContextMenu. The CommandTarget will therefore point to the ContextMenu.PlacementTarget (which is the Image in the example). The Image is an element of the Window visual tree where the CommandBinding is defined.
This is only necessary when the routed command is used inside a Popup (for example ContextMenu).
Otherwise setting the CommandTarget is not necessary.
<Window>
<StackPanel>
<!-- CommandTarget is not needed when the ICommandSource is part of the parent Window's visual tree -->
<Button Command="{x:Static local:MainWindow.ShowMyDialogCommand}" />
<Image>
<Image.ContextMenu>
<ContextMenu>
<!-- Visual tree is different from the Window (due to the Popup).
Set CommandTarget to allow the command to traverse the visual tree
of the MainWindow to reach to the CommandBindng (defined by the MainWindow) -->
<MenuItem Header="Open Window"
Command="{x:Static local:MainWindow.ShowMyDialogCommand}"
CommandTarget="{Binding RelativeSource={RelativeSource
AncestorType=ContextMenu}, Path=PlacementTarget}" />
</ContextMenu>
</Image.ContextMenu>
</Image>
</StackPanel>
</Window>
MyDialog.xaml.cs
partial class MyDialog : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
// Setting the DialogResult will automatically close the Window
// and return the DialogResult value.
this.DialogResult = true;
}
}
MyDialog.xaml
It's important to set Button.IsCancel to true for the "Cancel" button.
This allows the Window to close itself automatically.
Closing the Window in case of the "Ok" button being clicked is achieved by setting the Window.DialogResult property from a Button.Click handler (or RoutedCommand). Window will always close itself when Window.DialogResult is set.
<Window>
<Grid>
<Grid.RowDefinitions>
<RowDefinition /> <!-- Content row -->
<RowDefinition Height="Auto" /> <!-- Dialog button row -->
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="I'm a custom dialog" />
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="Ok"
IsDefault="True"
Click="OkButton_Click"/>
<Button Content="Cancel"
IsCancel="True" />
</StackPanel>
</Grid>
</Window>
Advanced MVVM compliant dialog flow
A more advanced version will make use of the fact that the Window is a ContentControl. This means we can define the content based on a data model (like the above MyDialogViewModel) and load the associated view by defining a DataTemplate, preferably implicit (without the x:Key directive defined). This makes the dialog highly reusable and easy to deal with in an MVVM context.
The following example defines a dialog that only knows how to handle an "Ok" and "Cancel" button. But through data templating the same class can show all kind of views.
IOkDialogViewModel.cs
interface IOkDialogViewModel
{
bool CanExecuteOkCommand();
}
OkDialogViewModel.cs
Example data model that is mapped to a dedicated view via a DataTemplate
that makes the content of the dialog.
// Consider to implement INotifyDataErrorInfo
public class OkDialogViewModel : IOkDialogViewModel, INotifyPropertyChanged
{
public string SomeText { get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
public bool CanExecuteOkCommand() => this.SomeText.StartsWith("#");
}
OkDialog.xaml.cs
public partial class OkDialog : Window
{
public static RoutedCommand OkCommand { get; } = new RoutedCommand("OkCommand", typeof(MainWindow));
public OkDialog()
{
InitializeComponent();
var okCommandBinding = new CommandBinding(OkDialog.OkCommand, ExecuteOkCommand, CanExecuteOkCommand);
this.CommandBindings.Add(okCommandBinding);
this.DataContextChanged += OnDataContextChanged;
}
// If there is no explicit Content set, use the DataContext
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) => this.Content ??= e.NewValue;
private void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = (this.DataContext as IOkDialogViewModel)?.CanExecuteOkCommand() ?? true;
private void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e)
=> this.DialogResult = true;
}
OkDialog.xaml
Now hardcode the default content (the "Ok" and "Close" buttons) into the Window.Template. This will make the static content.
The dynamic content is implicitly created by the client who defined a DataTemplate for the Window.Content.
<Window>
<Window.Template>
<ControlTemplate TargetType="Window">
<Grid>
<Grid.RowDefinitions>
<RowDefinition /> <!-- Dynamic content row -->
<RowDefinition Height="Auto" /> <!-- Static content row (ok and cancel buttons etc.) -->
</Grid.RowDefinitions>
<!-- Dynamic content -->
<ContentPresenter Grid.Row="0" />
<!-- Static content -->
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="Ok"
IsDefault="True"
Command="{x:Static local:OkDialog.OkCommand}" />
<Button Content="Cancel"
IsCancel="True" />
</StackPanel>
</Grid>
</ControlTemplate>
</Window.Template>
</Window>
App.xaml
Define a DataTemplate to crate the particular dialog view that is associated with the OkDialogViewModel.
<Application>
<Application.Resources>
<DataTemplate DataType="{x:Type local:OkDialogViewModel}">
<TextBox Text="{Binding SomeText}" />
</DataTemplate>
</Application.Resources>
</Application>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public static RoutedCommand ShowMyDialogCommand { get; } = new RoutedCommand("ShowMyDialogCommand", typeof(MainWindow));
private MainWindowViewModel MainWindowViewModel { get; }
public MainWindow()
{
InitializeComponent();
this.MainWindowViewModel = new MainWindowViewModel();
this.DataContext = this.MainWindowViewModel;
var showMyDialogCommandBinding = new CommandBinding(ShowMyDialogCommand, ExecuteShowMyDialogCommand, CanExecuteShowMyDialogCommand);
this.CommandBindings.Add(showMyDialogCommandBinding);
}
private void CanExecuteShowMyDialogCommand(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true;
private void ExecuteShowMyDialogCommand(object sender, ExecutedRoutedEventArgs e)
{
// Because the text doesn't start with '#', the OK button will be disabled later,
// until the user fixes the input in the TextBox.
var dialogViewModel = new OkDialogViewModel() { SomeText = "Just some text" };
var okDialog = new OkDialog()
{
Title = "I'm an Ok dialog",
DataContext = dialogViewModel
};
bool? dialogResult = okDialog.ShowDialog();
// Do something when the user has closed the dialog e.g. using the 'OK' button
if (dialogResult == true)
{
// Pass the dialog data (if it has some) to the view model class
// for further processing. The data is stored via data binding in the
// DataContext/Content of the dialog.
// Depending on the context of the dialog, the dialog's view model
// knows what to do with the data (e.g. save it to a database using the Model).
this.MainWindowViewModel.HandleData(dialogViewModel);
}
}
}
MainWindow.xaml
Because the ContextMenu will have its own visual tree (it uses a Popup to display content), the routed command must be executed in the visual tree of the parent Window. For this reason we must explicitly set the MenuItem.CommandTarget property to point to the visual tree outside of the ContextMenu. The CommandTarget will therefore point to the ContextMenu.PlacementTarget (which is the Image in the example). The Image is an element of the Window visual tree where the CommandBinding is defined.
This is only necessary when the routed command is used inside a Popup (for example ContextMenu).
Otherwise setting the CommandTarget is not necessary.
<Window>
<StackPanel>
<!-- CommandTarget is not needed when the ICommandSource is part of the parent Window's visual tree -->
<Button Command="{x:Static local:MainWindow.ShowMyDialogCommand}" />
<Image>
<Image.ContextMenu>
<ContextMenu>
<!-- Visual tree is different from the Window (due to the Popup).
Set CommandTarget to allow the command to traverse the visual tree
of the MainWindow to reach to the CommandBindng (defined by the MainWindow) -->
<MenuItem Header="Open Window"
Command="{x:Static local:MainWindow.ShowMyDialogCommand}"
CommandTarget="{Binding RelativeSource={RelativeSource
AncestorType=ContextMenu}, Path=PlacementTarget}" />
</ContextMenu>
</Image.ContextMenu>
</Image>
</StackPanel>
</Window>
Update
It turns out that the origin is really the 3rd party library. The implementation of the control is obviously really bad. It freezes the UI during construction/loading which is unacceptable.
Because the UI is frozen you can't even show a busy indicator. The user is left to believe that the application has crashed.
Such a library would make me doubt the authors skills and experience.
Because of the serious impact on the application's performance and UX I recommend to find an alternative library.
Even closing the ContextMenu forcefully does not solve the problem of a bad UX as the application still hangs.
The following solution extends the previous "Advanced MVVM compliant dialog flow" example. Following the "Advanced MVVM compliant dialog flow" will give you a clean design that helps to solve the issue more "gracefully" (I still recommend to find a better library).
The solution implements the following flow:
Instead of opening the dialog (which contains the terrible control) directly on clicking the MenuItem, we modify the flow to first close the ContextMenu.
This is accomplished by registering a ContextMenu.Opened event handler.
Next we spawn a second UI thread. Because any busy indicator that runs in the primary UI thread would freeze too, we use this dedicated new UI thread to show a busy indicator dialog. This way we can improve the UX significantly as from the user's point of view everything appears to be under control: just some heavy loading in the background.
In the main UI tread we create the instance of the dialog which is known to freeze the application (which will still freeze)
We use a SemaphoreSlim to allow the busy indicator dialog to wait asynchronously for a signal from the main UI thread in order to continue.
After the busy indicator thread received the signal, the busy indicator will close itself and shut down the second UI thread
The dialog cantaining the 3rd party control is now ready to use.
MainWindow.xaml
<Window>
<Image>
<Image.ContextMenu>
<ContextMenu Closed="OnImageContextMenuClosed">
<!-- Visual tree is different from the Window (due to the Popup).
Set CommandTarget to allow the command to traverse the visual tree
of the MainWindow to reach to the CommandBindng (defined by the MainWindow) -->
<MenuItem Header="Open Window"
Command="{x:Static local:MainWindow.ShowMyDialogCommand}"
CommandTarget="{Binding RelativeSource={RelativeSource
AncestorType=ContextMenu}, Path=PlacementTarget}" />
</ContextMenu>
</Image.ContextMenu>
</Image>
</Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public static RoutedCommand ShowMyDialogCommand { get; } = new RoutedCommand("ShowMyDialogCommand", typeof(MainWindow));
private MainWindowViewModel MainWindowViewModel { get; }
public MainWindow()
{
InitializeComponent();
this.MainWindowViewModel = new MainWindowViewModel();
this.DataContext = this.MainWindowViewModel;
var showMyDialogCommandBinding = new CommandBinding(ShowMyDialogCommand, ExecuteShowMyDialogCommand, CanExecuteShowMyDialogCommand);
this.CommandBindings.Add(showMyDialogCommandBinding);
}
private void CanExecuteShowMyDialogCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = true;
// Only close the ContextMenu. The ContextMenu.Closed event will continue the flow.
private void ExecuteShowMyDialogCommand(object sender, ExecutedRoutedEventArgs e)
=> (e.OriginalSource as FrameworkElement).ContextMenu.IsOpen = false;
private void OnImageContextMenuClosed(object? sender, EventArgs e)
{
// Create a semaphore that is initially blocking.
// The semaphore is used to signal the new UI thread that it must shut down
// and close the busy indicator dialog.
using var semaphore = new SemaphoreSlim(0, 1);
var uiThread = new Thread(state => ShowBusyIndicator(semaphore))
{
IsBackground = false
};
uiThread.SetApartmentState(ApartmentState.STA);
uiThread.Start();
(bool IsOk, OkDialogViewModel ViewModel) dialogResult = ShowOkDialog(semaphore);
// Do something when the user has closed the dialog e.g. using the 'OK' button
if (dialogResult.IsOk)
{
//dialogResult.ViewModel
}
}
private void ShowBusyIndicator(SemaphoreSlim semaphore)
{
// Consider to create a dedicated BusyIndicatorDialog class (following the pattern of the OkDialog).
// This allows to create a DataTemplate to design the dialog using XAML.
var busyIndicator = new Window()
{
Content = new ProgressBar() { IsIndeterminate = true },
Title = "Loading, please wait..."
};
// Let the busy indicator dialog wait for the SemaphoreSlim to signal.
// Consider to move this code directly to a dedicated BusyIndicatorDialog class.
// In case of implementing a dedicated BusyIndicatorDialog, consider to implement a special event which allows more control over the timing of the event (to replace the Loaded event).
busyIndicator.Loaded += (s, e) => OnBusyIndicatorLoaded(busyIndicator, semaphore);
busyIndicator.Show();
Dispatcher.Run();
}
// Use the Dispatcher of the busy indicator Window to post the code to the second UI thread.
private void OnBusyIndicatorLoaded(Window busyIndicator, SemaphoreSlim semaphore)
{
_ = busyIndicator.Dispatcher.InvokeAsync(async () =>
{
// Wait for the signal to continue the thread.
await semaphore.WaitAsync();
busyIndicator.Close();
busyIndicator.Dispatcher.BeginInvokeShutdown(DispatcherPriority.ContextIdle);
}, DispatcherPriority.ContextIdle);
}
private (bool IsOk, OkDialogViewModel ViewModel) ShowOkDialog(SemaphoreSlim semaphore)
{
var dialogViewModel = new OkDialogViewModel() { SomeText = "Just some text" };
var myDialog = new OkDialog()
{
Title = "I'm a Ok dialog",
DataContext = dialogViewModel
};
// Signal the busy indicator thread to continue (it will close itself and shut down the thread)
_ = semaphore.Release();
bool dialogResult = myDialog.ShowDialog() ?? false;
return (dialogResult, dialogViewModel);
}
}
Related
`I am working on a WPF application (MVVM)
I have a user control(uc1) that has four buttons. cancel,accept,exit
I am going to use this control in multiple views.
I need to cancel button to revert the changes what user will make in propertygrig
user control:
<UserControl x:Class="WPF.CustomControl.RadPropertyWindowButtons"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="45" d:DesignWidth="700">
<Grid>
<Grid Uid="radpropertybuttons" Height="39" VerticalAlignment="Bottom" Margin="74,0,-108,0">
<Button x:Name="Cancel"
Command="{Binding radpropertyCancel}" >
</Button>
<Button x:Name="Accept"
Command="{Binding radpropertyAccept}">
</Button>
<Button x:Name="Exit"
Command="{Binding radpropertyExit}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}">
</Button>
</Grid>
</Grid>
</UserControl>
view:
<Grid Height="564" VerticalAlignment="Top" >
<Grid HorizontalAlignment="Center">
<telerik:RadLayoutControl
Name="PropertyGridContainer"
Orientation="Vertical">
</telerik:RadLayoutControl>
</Grid>
<Grid VerticalAlignment="Bottom">
<customcontrol:RadPropertyWindowButtons x:Name="ucPropertyButtons" Height="44" VerticalAlignment="Top" HorizontalAlignment="Center" Loaded="RadPropertyWindowButtons_Loaded" />
</Grid>
</Grid>
in view model
public ICommand radpropertyCancel { get; set; }
radpropertyCancel = new ViewModelCommand(execradpropertyCancel);
private void execradpropertyCancel(object obj)
{
this.RevertToOriginalData();
}
how to clear the PropertyGridContainer and bind with the data that we get from RevertToOriginalData`
I do it like this if i do from code behind if i use click event but how to do it with command.
this._viewModel.RevertToOriginalData();
this.PropertyGridContainer.Items.Clear();
this.PropertyGridContainer.Items.Add(this._viewModel.myGrid);
this.ViewModel.IsDirty = false;
this._viewModel.myGrid is wrong design if myGrid is really a Grid ( a UI element). Your view model classes must never handle UI elements or participate in/implement UI logic.
Data changes are always handled outside the view (where the data lives). Layout on the other hand is always the domain of the view.
If you want to revert the layout changes made by the user, you must do this completely in the view (code-behind).
To accomplish this, a parent control (e.g., Window) that hosts both, the RadPropertyWindowButtons and the RadLayoutControl, should expose the related commands as routed commands.
Then in the command handlers you save (serialize) the layout before edit (or alternatively on accept/after edit) and restore (deserialize) it in case the edit procedure was cancelled. The RadLayoutControl exposes a related API to help with the serialization.
Now, that the implementation of the custom control no longer depends on the explicit view model class type, the RadPropertyWindowButtons has become fully reusable in any context.
In general, to enable reusability of controls they must express their (data) dependencies as dependency properties, that are later bound to the current DataContext. The internals of the reusable control simply bind to these dependency properties (instead of binding to an explicit DataContext type). Otherwise they are only "reusable" with a particular DataContext.
MainWindow.xaml.cs
partial class MainWindow : Window
{
public static RoutedUICommand CancelEditLayoutCommand { get; } = new RoutedUICommand(
"Cancel layout edit and revert to previous state",
nameof(MainWindow.CancelEditLayoutCommand),
typeof(MainWindow));
public static RoutedUICommand AcceptLayoutCommand { get; } = new RoutedUICommand(
"Accept the current layout",
nameof(MainWindow.AcceptLayoutCommand),
typeof(MainWindow));
private Dictionary<RadLayoutControl, bool> IsInEditModeTable { get; }
private string SerializedLayout { get; set; }
public MainWindow()
{
InitializeComponent();
this.IsInEditModeTable = new Dictionary<RadLayoutControl, bool>();
var cancelEditLayoutCommandBinding = new CommandBinding(MainWindow.CancelEditLayoutCommand, ExecuteCancelEditLayoutCommand, CanExecuteCancelEditLayoutCommand);
_ = this.CommandBindings.Add(cancelEditLayoutCommandBinding);
var acceptLayoutCommandBinding = new CommandBinding(MainWindow.AcceptLayoutCommand, ExecuteAcceptLayoutCommand, CanExecuteAcceptLayoutCommand);
_ = this.CommandBindings.Add(acceptLayoutCommandBinding);
}
private void CanExecuteCancelEditLayoutCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = e.Parameter is RadLayoutControl targetControl
&& this.IsInEditModeTable.TryGetValue(targetControl, out bool isTargetControlInEditMode)
&& isTargetControlInEditMode;
private void ExecuteCancelEditLayoutCommand(object sender, ExecutedRoutedEventArgs e)
{
var targetControl = (RadLayoutControl)e.Parameter;
RestoreLayout(targetControl);
this.IsInEditModeTable[targetControl] = false;
}
private void CanExecuteAcceptLayoutCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = e.Parameter is RadLayoutControl targetControl
&& this.IsInEditModeTable.TryGetValue(targetControl, out bool isTargetControlInEditMode)
&& isTargetControlInEditMode;
private void ExecuteAcceptLayoutCommand(object sender, ExecutedRoutedEventArgs e)
{
var targetControl = (RadLayoutControl)e.Parameter;
SaveLayout(targetControl);
this.IsInEditModeTable[targetControl] = false;
}
// Instead of handling the SelectionChanged event I recommend
// to introduce another routed command that allows the user to put the RadLayoutControl into edit mode (by setting the RadLayoutControl.IsInEditMode accordingly).
// Aside from an improved UX this would provide a better flow or trigger to kickoff the serialization
private void OnLayoutControlSelectionChanged(object sender, LayoutControlSelectionChangedEventArgs e)
{
var targetControl = sender as RadLayoutControl;
if (this.IsInEditModeTable.TryGetValue(targetControl, out bool isTargetControlInEditMode)
&& isTargetControlInEditMode)
{
return;
}
isTargetControlInEditMode = e.NewItem is not null;
if (isTargetControlInEditMode)
{
SaveLayout(targetControl);
}
this.IsInEditModeTable[targetControl] = isTargetControlInEditMode;
}
private void SaveLayout(RadLayoutControl targetControl)
=> this.SerializedLayout = targetControl.SaveToXmlString();
private void RestoreLayout(RadLayoutControl targetControl)
=> targetControl.LoadFromXmlString(this.SerializedLayout);
}
MainWindow.xaml
<Window>
<StackPanel>
<telerik:RadLayoutControl Name="PropertyGridContainer"
IsInEditMode="True"
telerik:RadLayoutControl.SerializationId="PropertyGridContainerID"
SelectionChanged="OnLayoutControlSelectionChanged" />
<customcontrol:RadPropertyWindowButtons TargetControl="{Binding ElementName=PropertyGridContainer}" />
</StackPanel>
</Window>
RadPropertyWindowButtons.xaml.cs
class RadPropertyWindowButtons
{
public RadLayoutControl TargetControl
{
get => (RadLayoutControl)GetValue(TargetControlProperty);
set => SetValue(TargetControlProperty, value);
}
public static readonly DependencyProperty TargetControlProperty = DependencyProperty.Register(
"TargetControl",
typeof(RadLayoutControl),
typeof(RadPropertyWindowButtons),
new PropertyMetadata(default));
}
RadPropertyWindowButtons.xaml
<UserControl>
<StackPanel>
<Button x:Name="Cancel"
Command="{x:Static local:MainWindow.CancelEditLayoutCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=TargetControl}" />
<Button x:Name="Accept"
Command="{x:Static local:MainWindow.AcceptLayoutCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=TargetControl}" />
</StackPanel>
</UserControl>
See Save/Load Layout for more advanced scenarios.
I'm using Material Design for WPF to show a dialog which receives some inputs from the user, and I would like to return a value when it's closed. Here is the sample code:
VM's method that opens the dialog
private async void OnOpenDialog()
{
var view = new TakeInputDialogView();
var result = await DialogHost.Show(view, "RootDialog", ClosingEventHandler);
}
Dialog's VM code
public class TakeSomeInputDialogViewModel : ViewModelBase
{
private string _name;
public string Name
{
get => _name;
set
{
SetProperty(ref _name, value);
SaveCommand.RaiseCanExecuteChanged();
}
}
public bool IsNameInvalid => CanSave();
public DelegateCommand SaveCommand { get; }
public TakeSomeInputDialogViewModel()
{
SaveCommand = new DelegateCommand(OnSave, CanSave);
}
private void OnSave()
{
DialogHost.Close("RootDialog");
}
private bool CanSave()
{
return !string.IsNullOrEmpty(Name);
}
}
When the user clicks save I would like to return Name or some object that will be constructed depending on the user's input.
Side note: I'm also using Prism library, but decided to go with Material Design dialog because I can't locate the dialog in a correct place, when I open the dialog via prism I could only open it in the center of screen or in the center of owner, but I have a single window which hosts sidebar, menu control and content control, and I need to open the dialog in the middle of content control which I wasn't able to achieve.
P.S: I could bind the DataContext of the Dialog to the VM that opens it, but I might have many dialogs and the code might grow too big.
Never show a dialog from the View Model. This is not necessary and will eliminate the benefits MVVM gives you.
Rule of thumb
The MVVM dependency graph:
View ---> View Model ---> Model
Note that the dependencies are on application level. They are component dependencies and not class dependencies (although class dependencies derive from the constraints introduced by the component dependencies).
The above dependency graph translates to the following rules:
In MVVM the View Model does not know the View: the View is nonexistent.
Therefore, if the View is nonexistent for the View Model it is not aware of UI: the View Model is View agnostic.
As a consequence, the View Model has no interest in displaying dialogs. It doesn't know what a dialog is.
A "dialog" is an information exchange between two subjects, in this case the user and the application.
If the View Model is View agnostic it also has no idea of the user to have a dialog with.
The View Model doesn't show dialog controls nor does it handle their flow.
Code-behind is a compiler feature (partial class).
MVVM is a design pattern.
Because by definition a design pattern is language and compiler agnostic, it doesn't rely on or require language or compiler details.
This means a compiler feature can never violate a design pattern.
Therefore code-behind can't violate MVVM.
Since MVVM is a design pattern only design choices can violate it.
Because XAML doesn't allow to implement complex logic we will always have to come back to C# (code-behind) to implement it.
Also the most important UI design rule is to prevent the application from collecting wrong data. You do this by:
a) don't show input options that produce an invalid input in the UI. For example remove or disable the invalid items of a ComboBox, so taht the user can only select valid items.
b) use data validation and the validation feedback infrastructure of WPF: implement INotifyDataErrorInfo to let the View signal the user that his input is invalid (e.g. composition of a password) and requires correction.
c) use file picker dialogs to force the user to provide only valid paths to the application: the user can only pick what really exists in the filesystem.
Following the above principles
will eliminate the need of your application to actively interact with the user (to show dialogs) for 99% of all cases.
In a perfect application all dialogs should be input forms or OS controlled system dialogs.
ensure data integrity (which is even more important).
Example
The following complete example shows how to show dialogs without violating the MVVM design pattern.
It shows two cases
Show a user initiated dialog ("Create User")
Show an application initiated dialog (HTTP connection lost)
Because most dialogs have an "OK" and a "Cancel" button or only a "OK" button, we can easily create a single and reusable dialog.
This dialog is named OkDialog in the example and extends Window.
The example also shows how to implement a way to allow the application to actively communicate with the user, without violating the MVVM design rules.
The example achieves this by having the View Model expose related events that the View can handle. For example, the View can decide to show a dialog (e.g., a message box). In this example, the View will handle a ConnectionLost event raised by a View Model class. The View handles this event by showing a notification to the user.
Because Window is a ContentControl we make use of the ContentContrl.ContentTemplate property:
when we assign a data model to the Window.Content property and a corresponding DataTemplate to the Window.ContentTemplate property, we can create individually designed dialogs by using a single dialog type (OkDialog) as content host.
This solution is MVVM conform because View and View Model are still well separated in term of responsibilities.
Every solution is fine that follows the pattern WPF uses to display validation errors (event, exception or Binding based) or progress bars (usually Binding based).
It's important to ensure that the UI logic does not bleed into the View Model.
The View Model should never wait for a user response. Just like data validation doesn't make the View Model wait for valid input.
The example is easy to convert to support the Dependency Injection pattern.
Reusable key classes of the pattern
DialogId.cs
public enum DialogId
{
Default = 0,
CreateUserDialog,
HttpConnectionLostDialog
}
IOkDialogViewModel.cs
// Optional interface. To be implemented by a dialog view model class
interface IOkDialogViewModel : INotifyPropertyChanged
{
// The title of the dialog
string Title { get; }
// Use this to validate the current view model state/data.
// Return 'false' to disable the "Ok" button.
// This method is invoked by the OkDialog before executing the OkCommand.
bool CanExecuteOkCommand();
// Called after the dialog was successfully closed
void ExecuteOkCommand();
}
OkDialog.xaml.cs
public partial class OkDialog : Window
{
public static RoutedCommand OkCommand { get; } = new RoutedCommand("OkCommand", typeof(MainWindow));
public OkDialog(object contentViewModel)
{
InitializeComponent();
var okCommandBinding = new CommandBinding(OkDialog.OkCommand, ExecuteOkCommand, CanExecuteOkCommand);
_ = this.CommandBindings.Add(okCommandBinding);
this.DataContext = contentViewModel;
this.Content = contentViewModel;
this.DataContextChanged += OnDataContextChanged;
}
// If there is no explicit Content, use the DataContext
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) => this.Content ??= e.NewValue;
// If the content view model doesn't implement the optional IOkDialogViewModel just enable the command source.
private void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = (this.Content as IOkDialogViewModel)?.CanExecuteOkCommand() ?? true;
private void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e)
=> this.DialogResult = true;
}
OkDialog.xaml
Window Height="450" Width="800"
Title="{Binding Title}">
<Window.Template>
<ControlTemplate TargetType="Window">
<Grid>
<Grid.RowDefinitions>
<RowDefinition /> <!-- Content row (dynamic) -->
<RowDefinition Height="Auto" /> <!-- Dialog button row (static) -->
</Grid.RowDefinitions>
<!-- Dynamic content -->
<ContentPresenter Grid.Row="0" />
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="Ok"
IsDefault="True"
Command="{x:Static local:OkDialog.OkCommand}" />
<Button Content="Cancel"
IsCancel="True" /> <!-- Setting 'IsCancel' to 'true' will automaitcally close the dialog on click -->
</StackPanel>
</Grid>
</ControlTemplate>
</Window.Template>
</Window>
Helper classes to complete the example
MainWindow.xaml.cs
The dialog is always displayed from a component of the View.
partial class MainWindow : Window
{
// By creating a RoutedCommand, we conveniently enable every child control of this view to invoke the command.
// Based on the CommandParameter, this view will decide which dialog or dialog content to load.
public static RoutedCommand ShowDialogCommand { get; } = new RoutedCommand("ShowDialogCommand", typeof(MainWindow));
// Map dialog IDs to a view model class type
private Dictionary<DialogId, Type> DialogIdToViewModelMap { get; }
public MainWindow()
{
InitializeComponent();
var mainViewModel = new MainViewModel();
// Show a notification dialog to the user when the HTTP connection is down
mainViewModel.ConnectionLost += OnConnectionLost;
this.DataContext = new MainViewModel();
this.DialogIdToViewModelMap = new Dictionary<DialogId, Type>()
{
{ DialogId.CreateUserDialog, typeof(CreateUserViewModel) }
{ DialogId.HttpConnectionLostDialog, typeof(MainViewModel) }
};
// Register the routed command
var showDialogCommandBinding = new CommandBinding(
MainWindow.ShowDialogCommand,
ExecuteShowDialogCommand,
CanExecuteShowDialogCommand);
_ = CommandBindings.Add(showDialogCommandBinding);
}
private void CanExecuteShowDialogCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = e.Parameter is DialogId;
private void ExecuteShowDialogCommand(object sender, ExecutedRoutedEventArgs e)
=> ShowDialog((DialogId)e.Parameter);
private void ShowDialog(DialogId parameter)
{
if (!this.DialogIdToViewModelMap.TryGetValue(parameter, out Type viewModelType)
|| !this.MainViewModel.TryGetViewModel(viewModelType, out object viewModel))
{
return;
}
var dialog = new OkDialog(viewModel);
bool isDialogClosedSuccessfully = dialog.ShowDialog().GetValueOrDefault();
if (isDialogClosedSuccessfully && viewModel is IOkDialogViewModel okDialogViewModel)
{
// Because of data bindng the collected data is already inside the view model.
// We can now notify it that the dialog has closed and the data is ready to process.
// Implementing IOkDialogViewModel is optional. At this point the view model could have already handled
// the collected data via the PropertyChanged notification or property setter.
okDialogViewModel.ExecuteOkCommand();
}
}
private void OnConnectionLost(object sender, EventArgs e)
=> ShowDialog(DialogId.HttpConnectionLostDialog);
}
MainWindow.xaml
<Window>
<Button Content="Create User"
Command="{x:Static local:MainWindow.ShowDialogCommand}"
CommandParameter="{x:Static local:DialogId.CreateUserDialog}"/>
</Window>
App.xaml
The implicit DataTemplate for the content of the OkDialog.
<ResourceDictionary>
<!-- The client area of the dialog content.
"Ok" and "Cancel" button are fixed and not part of the client area.
This enforces a homogeneous look and feel for all dialogs -->
<DataTemplate DataType="{x:Type local:CreateUserViewModel}">
<TextBox Text="{Binding UserName}" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:MainViewModel}">
<TextBox Text="HTTP connection lost." />
</DataTemplate>
UserCreatedEventArgs.cs
public class UserCreatedEventArgs : EventArgs
{
public UserCreatedEventArgs(User createdUser) => this.CreatedUser = createdUser;
public User CreatedUser { get; }
}
CreateUserViewModel.cs
// Because this view model wants to be explicitly notified by the dialog when it closes,
// it implements the optional IOkDialogViewModel interface
public class CreateUserViewModel :
IOkDialogViewModel,
INotifyPropertyChanged,
INotifyDataErrorInfo
{
// UserName binds to a TextBox in the dialog's DataTemplate. (that targets CreateUserViewModel)
private string userName;
public string UserName
{
get => this.userName;
set
{
this.userName = value;
OnPropertyChanged();
}
}
public string Title => "Create User";
private DatabaseRepository Repository { get; } = new DatabaseRepository();
bool IOkDialogViewModel.CanExecuteOkCommand() => this.UserName?.StartsWith("#") ?? false;
void IOkDialogViewModel.ExecuteOkCommand()
{
var newUser = new User() { UserName = this.UserName };
// Assume that e.g. the MainViewModel observes the Repository
// and gets notified when a User was created or updated
this.Repository.SaveUser(newUser);
OnUserCreated(newUser);
}
public event EventHandler<UserCreatedEventArgs> UserCreated;
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnUserCreated(User newUser)
=> this.UserCreated?.Invoke(this, new UserCreatedEventArgs(newUser));
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public CreateUserViewModel CreateUserViewModel { get; }
public event EventHandler ConnectionLost;
private Dictionary<Type, object> ViewModels { get; }
private HttpService HttpService { get; } = new HttpService();
public MainViewModel()
{
this.CreateUserViewModel = new CreateUserViewModel();
// Handle the created User (optional)
this.CreateUserViewModel.UserCreated += OnUserCreated;
this.ViewModels = new Dictionary<Type, object>
{
{ typeof(CreateUserViewModel), this.CreateUserViewModel },
{ typeof(MainViewModel), this },
};
}
public bool TryGetViewModel(Type viewModelType, out object viewModel)
=> this.ViewModels.TryGetValue(viewModelType, out viewModel);
private void OnUserCreated(object? sender, UserCreatedEventArgs e)
{
User newUser = e.CreatedUser;
}
private void SendHttpRequest(Uri url)
{
this.HttpService.ConnectionTimedOut += OnConnectionTimedOut;
this.HttpService.Send(url);
this.HttpService.ConnectionTimedOut -= OnConnectionTimedOut;
}
private void OnConnectionTimedOut(object sender, EventArgs e)
=> OnConnectionLost();
private void OnConnectionLost()
=> this.ConnectionLost?.Invoke(this, EventArgs.Empt8y);
}
User.cs
class User
{
public string UserName { get; set; }
}
DatabaseRepository.cs
class DatabaseRepository
{}
HttpService.cs
class HttpService
{
public event EventHandler ConnectionTimedOut;
}
You only really need one window in a wpf application, because a window is a content control.
You can template out the entire content using an approach called viewmodel first and data templating.
That window would look like:
<Window ....
Title="{Binding Title}"
Content="{Binding}"
>
</Window>
You would then have a base window viewmodel which exposes a title property.
When you present a viewmodel to an instance of this window you'd by default just see the .ToString() of that viewmodel appear as content. To make it give you some controls you need a datatemplate.
You associate viewmodel type with control type using DataType.
Put all your markup in usercontrols. This includes your markup for mainwindow. At startup you can show an empty instance of TheOnlyWindowIneed then set datacontext asynchronously.
Then go get any data or do anything expensive you need. Once a skeleton mainwindow is up and visible. Showing a busy indicator.
Your datatemplates would all go in a resource dictionary which is merged in app.xaml.
An example
<DataTemplate DataType="{x:Type local:MyBlueViewModel}">
<local:MyBlueUserControl/>
</DataTemplate>
If you now do
var win = new TheOnlyWindowIneed { Content = new MyBlueViewModel() };
win.Owner=this;
win.ShowDialog();
Then you get a dialog shown which has the window that code is in as a parent. The datacontext is your MyBlueViewModel and your entire dialog is filled with a MyBlueUserControl.
You probably want Yes/No buttons and you probably want some standardised UI.
ConfirmationUserControl can look like:
<UserControl.....
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<ContentPresenter Content="{Binding ConfirmationMessage}"
Grid.Row="1"/>
<Button HorizontalAlignment="Left"
Content="Yes"
Command="{Binding YesCommand}"
/>
<Button HorizontalAlignment="Left"
Content="No"
Command="{Binding NoCommand}"
/>
</Grid>
</UserControl>
ConfirmationViewModel would expose the usual Title, ConfirmationMessage can be Another viewmodel ( and usercontrol pair ). This is then data templated out into UI in a similar fashion.
YesCommand would be a public iCommand which is set from whatever shows the dialog. This passes in whatever logic is to happen when the user clicks Yes. If it's deleting then it has code calls a delete method. It could use a lambda and capture the context of the owning viewmodel or it could have an explicit instance of a class passed in. The latter being more unit test friendly.
The call to this dialog is at the end of some code you have in your viewmodel.
There is no code waiting for the result.
The code that would be is in yescommand.
NoCommand might just close the parent window. By putting code that actions the result in yes command you probably do not need to "know" in the calling code what the user chose. It's already been handled.
You might therefore decide NoCommand uses some generic window closing approach:
public class GenericCommands
{
public static readonly ICommand CloseCommand =
new RelayCommand<Window>(o =>
{
if(o == null)
{
return;
}
if(o is Window)
{
((Window)o).Close();
}
}
);
That needs a reference to the window which is passed in by parameter
Command="{x:Static ui:GenericCommands.CloseCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
The viewmodel still needs to somehow initiate showing the window.
You only have the one window though. You can put code in that and initiate showing a window from the one and only window.
All you need at a minimum is a dependency property and a callback.
public partial class TheOnlyWindowIneed : Window
{
public object? ShowAdialogue
{
get
{
return (object?)GetValue(ShowAdialogueProperty);
}
set
{
SetValue(ShowAdialogueProperty, value);
}
}
public static readonly DependencyProperty ShowAdialogueProperty =
DependencyProperty.Register("ShowAdialogue",
typeof(object),
typeof(TheOnlyWindowIneed),
new FrameworkPropertyMetadata(null
, new PropertyChangedCallback(ShowAdialogueChanged)
)
);
private static void ShowAdialogueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var win = new TheOnlyWindowIneed { Content = e.NewValue };
win.Owner = d as TheOnlyWindowIneed;
win.ShowDialog();
}
Here when you bind ShowAdialogue to a property in your viewmodel, when you set that property to an instance of a dialogue viewmodel, it should show a dialog with that viewmodel as datacontext and as explained that will be templated out into UI.
A perhaps more elegant way to handle this would be to use the pub/sub pattern and the community mvvm toolkit messenger. You can send the viewmodel you want to show in a dialogue via messenger, subscribe in mainwindow and act in that handler.
If you just want to show some info then there is another option to consider. If all your users are using win10+ then you can easily use toast. The toast that pops up can have buttons in it but is usually just used to show messages.
Add the nuget package Notifications.wpf
You can show toast in your mainwindow or where notifications usually pop up in win10. In mainwindow:
<toast:NotificationArea x:Name="MainWindowNotificationsArea"
Position="BottomRight"
MaxItems="3"
Grid.Column="1"/>
In a viewmodel:
var notificationManager = new NotificationManager();
notificationManager.Show(
new NotificationContent {
Title = "Transaction Saved",
Message = $"Transaction id {TransactionId}" },
areaName: "MainWindowNotificationsArea");
The view and viewmodel are totally decoupled, you don't need any reference to anything. NotificationContent can be much richer than shown here.
I think your app might also have to target win10 as minimum O/S.
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>
We have a WPF application written using the MVVM pattern. Within the application is a TabControl with different UserControls within each tab. Under certain conditions one of the UserControls on a tab can take a significant portion of time to load when switching to the containing tab.
This is NOT because of any performance bottlenecks in the ViewModel. But instead, is due to significant amount of time that the usercontrol takes to bind to the ViewModel, and to create the various UI elements contained within it and initialize them.
When the user clicks on the tab for this usercontrol, the UI becomes completely unresponsive until the control has completed loading. If fact you don't even see the "active tab" switch until everything is loaded.
What strategies could I use to display a "spinner" with some sort of "please wait, loading..." message while waiting for the UI elements to complete loading?
UPDATE with sample code:
The below demonstrates the type of problem I am trying to get around. When you click on the "slow tab". The UI becomes unresponsive until all the items in the slow tab have rendered.
In the below, TestVM is the viewmodel for the slow tab. It has a large collection of children objects. Each created with it's own data template.
How could I display a "loading" message while the slow tab finishes loading?
public class MainVM
{
private TestVM _testVM = new TestVM();
public TestVM TestVM
{
get { return _testVM; }
}
}
/// <summary>
/// TestVM is the ViewModel for the 'slow tab'. It contains a large collection of children objects that each will use a datatemplate to render.
/// </summary>
public class TestVM
{
private IEnumerable<ChildBase> _children;
public TestVM()
{
List<ChildBase> list = new List<ChildBase>();
for (int i = 0; i < 100; i++)
{
if (i % 3 == 0)
{
list.Add(new Child1());
}
else if (i % 3 == 1)
{
list.Add(new Child2());
}
else
{
list.Add(new Child3());
}
}
_children = list;
}
public IEnumerable<ChildBase> Children
{
get { return _children; }
}
}
/// <summary>
/// Just a base class for a randomly positioned VM
/// </summary>
public abstract class ChildBase
{
private static Random _rand = new Random(1);
private int _top = _rand.Next(800);
private int _left = _rand.Next(800);
public int Top { get { return _top; } }
public int Left { get { return _left; } }
}
public class Child1 : ChildBase { }
public class Child2 : ChildBase { }
public class Child3 : ChildBase { }
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication3"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<!-- Template for the slow loading tab -->
<DataTemplate DataType="{x:Type local:TestVM}">
<ItemsControl ItemsSource="{Binding Children}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"></Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="FrameworkElement">
<Setter Property="Canvas.Top" Value="{Binding Top}"></Setter>
<Setter Property="Canvas.Left" Value="{Binding Left}"></Setter>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</DataTemplate>
<!-- examples of different child templates contained in the slow rendering tab -->
<DataTemplate DataType="{x:Type local:Child1}">
<DataGrid></DataGrid><!--simply an example of a complex control-->
</DataTemplate>
<DataTemplate DataType="{x:Type local:Child2}">
<RichTextBox Width="30" Height="30">
<!--simply an example of a complex control-->
</RichTextBox>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Child3}">
<Calendar Height="10" Width="15"></Calendar>
</DataTemplate>
</Window.Resources>
<Grid>
<TabControl>
<TabItem Header="Fast Loading tab">
<TextBlock Text="Not Much Here"></TextBlock>
</TabItem>
<TabItem Header="Slow Tab">
<ContentControl Content="{Binding TestVM}"></ContentControl>
</TabItem>
</TabControl>
</Grid>
</Window>
What do u need is here
http://msdn.microsoft.com/en-us/library/ms741870.aspx
public partial class Window1 : Window
{
// Delegates to be used in placking jobs onto the Dispatcher.
private delegate void NoArgDelegate();
private delegate void OneArgDelegate(String arg);
// Storyboards for the animations.
private Storyboard showClockFaceStoryboard;
private Storyboard hideClockFaceStoryboard;
private Storyboard showWeatherImageStoryboard;
private Storyboard hideWeatherImageStoryboard;
public Window1(): base()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Load the storyboard resources.
showClockFaceStoryboard =
(Storyboard)this.Resources["ShowClockFaceStoryboard"];
hideClockFaceStoryboard =
(Storyboard)this.Resources["HideClockFaceStoryboard"];
showWeatherImageStoryboard =
(Storyboard)this.Resources["ShowWeatherImageStoryboard"];
hideWeatherImageStoryboard =
(Storyboard)this.Resources["HideWeatherImageStoryboard"];
}
private void ForecastButtonHandler(object sender, RoutedEventArgs e)
{
// Change the status image and start the rotation animation.
fetchButton.IsEnabled = false;
fetchButton.Content = "Contacting Server";
weatherText.Text = "";
hideWeatherImageStoryboard.Begin(this);
// Start fetching the weather forecast asynchronously.
NoArgDelegate fetcher = new NoArgDelegate(
this.FetchWeatherFromServer);
fetcher.BeginInvoke(null, null);
}
private void FetchWeatherFromServer()
{
// Simulate the delay from network access.
Thread.Sleep(4000);
// Tried and true method for weather forecasting - random numbers.
Random rand = new Random();
String weather;
if (rand.Next(2) == 0)
{
weather = "rainy";
}
else
{
weather = "sunny";
}
// Schedule the update function in the UI thread.
tomorrowsWeather.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.Normal,
new OneArgDelegate(UpdateUserInterface),
weather);
}
private void UpdateUserInterface(String weather)
{
//Set the weather image
if (weather == "sunny")
{
weatherIndicatorImage.Source = (ImageSource)this.Resources[
"SunnyImageSource"];
}
else if (weather == "rainy")
{
weatherIndicatorImage.Source = (ImageSource)this.Resources[
"RainingImageSource"];
}
//Stop clock animation
showClockFaceStoryboard.Stop(this);
hideClockFaceStoryboard.Begin(this);
//Update UI text
fetchButton.IsEnabled = true;
fetchButton.Content = "Fetch Forecast";
weatherText.Text = weather;
}
private void HideClockFaceStoryboard_Completed(object sender,
EventArgs args)
{
showWeatherImageStoryboard.Begin(this);
}
private void HideWeatherImageStoryboard_Completed(object sender,
EventArgs args)
{
showClockFaceStoryboard.Begin(this, true);
}
}
P.S. Perhaps it is useful as well http://tech.pro/tutorial/662/csharp-tutorial-anonymous-delegates-and-scoping and Make dispatcher example to work
Make your control lazy-load its contents.
To do that expose an ObservableCollection property in your TestVM class and attach event handlers to CollectionChanged (possible PropertyChanged too) to add actual UI elements.
In Window1 prepare the data to load in TestVM on a separate thread (are you doing any web queries?), pass the data to TestVM on the UI thread.
If TestVM child controls themselves load slowly you can split that drive that process from a separate thread too but that's (way) more difficult to pull of, so hopefully it's the data loading that's the slow part
Causes could be Slow Code in a Binding Converter, Coerce Value Callback, Properties could all make a binding appear slow. For Example, Consider An Image whose source binds to a URL. This could load slow due to network lags.
Also avoid switching to dispatcher context - unless really needed. For example to launch threads, Waiting for WaitHandles, even large/slow synchronous I/O operations etc etc
Sten Petrov's suggestion to lazy load (UI and Data Virtualization) is vital too.
I have a WPF app with MVVM pattern and it contains following 2 views:
1:MainWindow.xaml (it's a window)
below is main portion of MainWindow.xaml:
<Window.Resources>
<DataTemplate DataType="{x:Type vm:XliffListViewModel}">
<vw:XliffListView />
</DataTemplate>
</Window.Resources>
<Grid Margin="4">
<Border Background="GhostWhite" BorderBrush="LightGray" BorderThickness="1" CornerRadius="5" >
<ItemsControl ItemsSource="{Binding ViewModels}" Margin="4" />
</Border>
</Grid>
2:XliffListView.xaml (it's a user control)
XliffListView contain a datagrid and a button for save all changes that happens
I want to show messagebox when user closing app if changes not saved
You can write and Attached behavior that handles Window.Closing() event of the Window and executes the ClosingCommand from ViewModel and returns true or false as parameter so that one can cancel (e.Cancel) closing event if the VM wants to stop th window from closing.
The code below is just for concept and is not complete
XAML
<Window x:Class="..."
...
myBheaviors:WindowBehaviors.ClosingCommand="{Binding MyClosingCommand}">
...
</Window>
ViewModel
public class MyWindowViewModel
{
public ICommand MyClosingCommand
{
get
{
if (_myClosingCommand == null)
_myClosingCommand
= new DelegateCommand<CancelEventArgs>(OnClosing);
return _myClosingCommand;
}
}
private void OnClosing(CancelEventArgs e)
{
if (this.Dirty) //Some function that decides if the VM has pending changes.
e.Cancel = true;
}
}
Attached Behavior
public static class WindowBehaviors
{
public static readonly DependencyProperty ClosingCommandProperty
= DependencyProperty.RegistsrrAttached(
"ClosingCommand",
...,
new PropertyMetataData(OnClosingCommandChanged);
public static ICommand GetClosingCommand(...) { .. };
public static void SetClosingCommand(...) { .. };
private static void OnClosingCommandChanged(sender, e)
{
var window = sender as Window;
var command = e.NewValue as ICommand;
if (window != null && command != null)
{
window.Closing
+= (o, args) =>
{
command.Execute(args);
if (args.Cancel)
{
MessageBox.Show(
"Window has pending changes. Cannot close");
// Now window will be stopped from closing.
}
};
}
}
}
EDIT:
For user controls, instead of Closing use Unloaded event.
Also try to establish a hierarchy between ViewModels i.e.e Window's ViewModel should contain UserControl's ViewModel. So that Closing event's IsDirty() call can scrutinize the UserControls' ViewModel also.