I have a desktop app already released (so I\'d appreciate an answer that keeps the changes and regression tests at a minimum) and I need to add a consistency check Can
What I've done till now is
against MVVM, because I've put the alert in the Model
The solution from @Tesseract, subclassing ObservableCollection
and subscribing RemoveItem
is already MVVM oriented.
What is still missing is a correct, modern way to send the alert from the ViewModel. This is where Mahapps approach would be helpful.
In your XAML
Dialog:DialogParticipation.Register="{Binding}"
where the attached property DialogPartecipation will keep track of the views through a dictionary
public static class DialogParticipation
{
private static readonly IDictionary<object, DependencyObject> ContextRegistrationIndex = new Dictionary<object, DependencyObject
The DialogCoordinator will match the ViewModel to the View
public class DialogCoordinator : IDialogCoordinator
{
public static readonly IDialogCoordinator Instance = new DialogCoordinator();
through the above said attached property (the context
being the view model)
var association = DialogParticipation.GetAssociation(context);
and display the dialog, invoking the appropriate method on the retrieved view: this is how, if you have multiple windows open, the dialog will display on the correct window.
If only the ObservableCollection implemented a "previewCollectionChanged", things would be so much easier.
For your needs I would recommend creating a subclass of ObservableCollection and overloading the protected method RemoveItem.
Depending on what you are doing with your application, you might want to override other methods than just RemoveItem (like ClearItems).
When subclassing ObservableCollection, there are 5 protected methods you can override : ClearItems, RemoveItem, InsertItem, SetItem and MoveItem.
These methods are, in the end, called by all the public methods, so overriding them gives you full control.
Here's a small app you can run that demonstrates this :
public class ObservableCollectionWithDeletionControl<T> : ObservableCollection<T>
{
public delegate void DeletionDeniedEventHandler(object sender, int indexOfDeniedDeletion);
public event DeletionDeniedEventHandler DeletionDenied;
public bool CanDelete { get; set; }
protected virtual void OnDeletionDenied(int indexOfDeniedDeletion)
{
if (DeletionDenied != null) { DeletionDenied(this, indexOfDeniedDeletion); }
}
protected override void RemoveItem(int index)
{
if (CanDelete)
{
base.RemoveItem(index);
}
else
{
OnDeletionDenied(index);
}
}
}
I use the DeletionDenied event so that this class is not responsible for showing the error window, and it makes it more reusable.
public class MainWindowViewModel
{
public MainWindow window { get; set; }
public ObservableCollectionWithDeletionControl<Person> People { get; set; } = new ObservableCollectionWithDeletionControl<Person>();
public MainWindowViewModel()
{
People.DeletionDenied += People_DeletionDenied;
}
private void People_DeletionDenied(object sender, int indexOfDeniedDeletion)
{
Person personSavedFromDeletion = People[indexOfDeniedDeletion];
window.displayDeniedDeletion(personSavedFromDeletion.Name);
}
}
The ViewModel for the MainWindow.
It knows it's window for the sole purpose of displaying the error message.
(I'm sure there's a better solution than this out there but I have yet to find a good and short way of displaying popup windows in mvvm.)
When the DeletionDenied event triggers, the error window is called.
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get { return _name; }
set
{
if(_name == value) { return; }
_name = value;
if( PropertyChanged != null ) { PropertyChanged(this, new PropertyChangedEventArgs("Name")); }
}
}
private string _name = "";
}
<Window x:Class="WpfApplication1.MainWindow"
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:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<CheckBox DockPanel.Dock="Top" Content="Can delete" IsChecked="{Binding People.CanDelete}" Margin="5" HorizontalAlignment="Left"/>
<DataGrid ItemsSource="{Binding People}" Margin="5,0"/>
</DockPanel>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public void displayDeniedDeletion(string name)
{
TextBox errorMessage = new TextBox();
errorMessage.Text = string.Format("Cannot delete {0} : access denied !", name);
Window popupErrorMessage = new Window();
popupErrorMessage.Content = errorMessage;
popupErrorMessage.ShowDialog();
}
}
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
MainWindow window = new MainWindow();
MainWindowViewModel viewModel = new MainWindowViewModel();
viewModel.window = window;
window.DataContext = viewModel;
window.Show();
App.Current.MainWindow = window;
}
}
I've set the ViewModel's window in the startup, but you should probably do it wherever you create your ViewModel
Maybe it's ugly (really, it's not so ugly: imho it's a nice MVVM approach, also applicable to modern mahapps.metro Dialogs
), but now I'm setting
if (!CanBeDeleted(out validate_msg)) {
PurchaseItem.MessageBoxText = validate_msg;
an invisible TextBox
<TextBox Visibility="Hidden" Name="tb_message"
Text="{Binding MessageBoxText}"
TextChanged="TextBox_TextChanged"
where I'm dispatching the alert
void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
string alert = tb_message.Text;
if (alert != null && tb_message.Text.Length>0) {
Dispatcher.BeginInvoke(
(Action)(() => {
MessageBox.Show(alert, "Alert", MessageBoxButton.OK);
tb_message.Text = "";
}));
}
}
I see a connection with this other question Prevent adding the new Item on ObservableCollection.CollectionChanged event, in my case I'd say that prevent deleting is even more important. I don't know if there are more updated answers than this one (Can I rollback collection changes on collection changed event? which appears wrong) about this topic.
While the PropertyChanged
can be easily raised to rollback an item update, for the collection changes I've been forced to pass and reference the view dispatcher inside the CollectionChanged
event
PurchaseItem.dispatcher.BeginInvoke((Action)(() => RollBack(args)));
to rollback the added/deleted items
bool rollingBack = false;
private void RollBack(NotifyCollectionChangedEventArgs args) {
rollingBack = true;
if (args.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var element in args.OldItems) {
PosInLocationsList.Add((PosInLocation)element);
}
}
if (args.Action == NotifyCollectionChangedAction.Add)
{
foreach (var element in args.NewItems) {
PosInLocationsList.Remove((PosInLocation)element);
}
}
rollingBack = false;
}