可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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
- against MVVM, because I've put the alert in the Model (but I can accept this, if it makes the changes quick and simple)
- 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.