add a check with MessageBox when DataGrid is chang

2019-01-20 14:48发布

问题:

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 CanBeDeleted when the grid is changed.

<DataGrid AutoGenerateColumns="False" 
    ItemsSource="{Binding CurrentPosIn.PosInLocationsList}" 
    CanUserAddRows="{Binding UpdateEnabled}" CanUserDeleteRows="{Binding UpdateEnabled}" >

I'm using UpdateEnabled for something different (profile permissions) and I don't want to make the DataGrid read only either: I'd prefer (unless it is too complex) to see a blocking alert (a MessageBox) preventing changes.

What I've done till now is

  1. against MVVM, because I've put the alert in the Model (but I can accept this, if it makes the changes quick and simple)
  2. not working, because the second part (see below) of my changes produces an invalid operation exception

The ViewModel contains the following list

    [Association(ThisKey="Partita", OtherKey="Partita", CanBeNull=true, IsBackReference=true)]
    public ObservableCollection<Model.PosInLocation> posin_locations_list = new  ObservableCollection<Model.PosInLocation>(); 
    public ObservableCollection<PosInLocation> PosInLocationsList {
        get { return posin_locations_list; }
        set { 
            posin_locations_list = value;
            OnPropertyChanged( () => PosInLocationsList );
        }
    } 

and I'm adding the consistency check here

    string _storage;
    [Column(Name = "storage"), PrimaryKey]
    public string Storage {
        get { return _storage; }
        set { 
            if (this.loadedEF) {
                string validate_msg;
                if (!PurchasePosIn.CanBeDeleted(out validate_msg)) {
                    // against MVVM
                    MessageBox.Show(validate_msg, "Alert", MessageBoxButton.OK); 
                    OnPropertyChanged( () => Storage  );
                    return;
                }
                Persistence.MyContext.deletePosInLocation(this);
            }
            _storage = value;
            OnPropertyChanged( () => Storage  );
            if (this.loadedEF) {
                Persistence.MyContext.insertPosInLocation(this);
            }               
        }
    }

and here (second part)

    internal void posin_locations_list_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs args)
    {                   
        string validate_msg;
        if (!CanBeDeleted(out validate_msg)) {
            // indirectly produces an invalid operation exception 
            MessageBox.Show(validate_msg, "Alert", MessageBoxButton.OK);
            return;
        }
        if (args.OldItems != null)
            foreach(var oldItem in args.OldItems) {
                if ( ((PosInLocation)oldItem).Partita != null)        
                    Persistence.MyContext.deletePosInLocation((PosInLocation)oldItem);              
            }       
        if (args.NewItems != null)
            foreach(var newItem in args.NewItems)
            {
                PosInLocation newPosInLocation = (PosInLocation)newItem;
                if ( newPosInLocation.Partita == null) {
                    newPosInLocation.Partita = this.Partita;
                    newPosInLocation.PurchasePosIn = this;
                    newPosInLocation.loadedEF = true;
                }
             }
    }

回答1:

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 :

ObservableCollection SubClass

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.

ViewModel

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.

Model

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 = "";
}


XAML

<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>


XAML.CS

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();
    }
}

APP MAIN

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




回答2:

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 = "";
                         }));
        }
    }

Rolling back added/deleted items

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;
    }


回答3:

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.

  • Use an attached property in your Window to register your view model with the dialog sub-system.

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
  • You can instantiate DialogCoordinator directly or inject the interface IDialogCoordinator into their view model

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.



标签: c# wpf mvvm