Ho già rilasciato un’app desktop (quindi gradirei una risposta che mantenga le modifiche e i test di regressione come minimo) e ho bisogno di aggiungere un controllo di coerenza CanBeDeleted
quando la griglia viene modificata.
Sto usando UpdateEnabled
per qualcosa di diverso (permessi di profilo) e non voglio rendere la lettura di DataGrid
solo: preferirei (a meno che non sia troppo complesso ) vedere un avviso di blocco (un MessageBox
) che impedisce le modifiche.
Quello che ho fatto fino ad ora è
ViewModel contiene il seguente elenco
[Association(ThisKey="Partita", OtherKey="Partita", CanBeNull=true, IsBackReference=true)] public ObservableCollection posin_locations_list = new ObservableCollection(); public ObservableCollection PosInLocationsList { get { return posin_locations_list; } set { posin_locations_list = value; OnPropertyChanged( () => PosInLocationsList ); } }
e sto aggiungendo il controllo di coerenza qui
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); } } }
e qui (seconda parte)
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; } } }
Se solo ObservableCollection implementasse un “previewCollectionChanged”, le cose sarebbero molto più semplici.
Per le tue esigenze ti consiglio di creare una sottoclass di ObservableCollection e di sovraccaricare il metodo protetto RemoveItem.
A seconda di ciò che si sta facendo con la propria applicazione, si potrebbe voler sovrascrivere altri metodi oltre a RemoveItem (come ClearItems).
Quando sottoclassi ObservableCollection, ci sono 5 metodi protetti che puoi sovrascrivere: ClearItems, RemoveItem, InsertItem, SetItem e MoveItem.
Questi metodi, alla fine, vengono chiamati da tutti i metodi pubblici, quindi sovrascriverli ti dà il pieno controllo.
Ecco una piccola app che puoi eseguire dimostrando questo:
public class ObservableCollectionWithDeletionControl : ObservableCollection { 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); } } }
Uso l’evento DeletionDenied in modo che questa class non sia responsabile della visualizzazione della finestra di errore e la rende più riutilizzabile.
public class MainWindowViewModel { public MainWindow window { get; set; } public ObservableCollectionWithDeletionControl People { get; set; } = new ObservableCollectionWithDeletionControl (); public MainWindowViewModel() { People.DeletionDenied += People_DeletionDenied; } private void People_DeletionDenied(object sender, int indexOfDeniedDeletion) { Person personSavedFromDeletion = People[indexOfDeniedDeletion]; window.displayDeniedDeletion(personSavedFromDeletion.Name); } }
ViewModel per MainWindow.
Sa che è la finestra al solo scopo di visualizzare il messaggio di errore.
(Sono sicuro che ci sia una soluzione migliore di questa, ma devo ancora trovare un modo buono e breve per visualizzare windows popup in mvvm.)
Quando si triggers l’evento DeletionDenied, viene richiamata la finestra di errore.
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 = ""; }
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; } }
Ho impostato la finestra di ViewModel all’avvio, ma probabilmente dovresti farlo ovunque crei ViewModel
Forse è brutto (davvero, non è così brutto: imho è un bel approccio MVVM , applicabile anche ai moderni makpps.metro Dialogs
), ma ora sto impostando
if (!CanBeDeleted(out validate_msg)) { PurchaseItem.MessageBoxText = validate_msg;
un TextBox
invisibile
dove sto spedendo l'avviso
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 = ""; })); } }
Vedo una connessione con quest'altra domanda Prevenire l'aggiunta del nuovo Item sull'evento ObservableCollection.CollectionChanged , nel mio caso direi che prevenire l'eliminazione è ancora più importante. Non so se ci sono risposte più aggiornate di questa ( posso ripristinare le modifiche alla raccolta sull'evento modificato della raccolta? Che sembra sbagliato ) su questo argomento.
Mentre il PropertyChanged
può essere facilmente sollevato per il rollback di un aggiornamento di un elemento, per le modifiche alla collezione sono stato costretto a passare e fare riferimento al dispatcher della vista all'interno dell'evento CollectionChanged
PurchaseItem.dispatcher.BeginInvoke((Action)(() => RollBack(args)));
per ripristinare gli elementi aggiunti / eliminati
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; }
Quello che ho fatto fino ad ora è
contro MVVM, perché ho messo l’avviso nel modello
La soluzione di @Tesseract, sottoclass di ObservableCollection
e sottoscrizione di RemoveItem
è già orientata MVVM.
Quello che manca ancora è un modo corretto e moderno per inviare l’avviso dal ViewModel. È qui che l’approccio di Mahapps sarebbe utile.
Nel tuo XAML
Dialog:DialogParticipation.Register="{Binding}"
dove la proprietà associata DialogPartecipation terrà traccia delle viste attraverso un dizionario
public static class DialogParticipation { private static readonly IDictionary
Il DialogCoordinator abbinerà ViewModel alla View
public class DialogCoordinator : IDialogCoordinator { public static readonly IDialogCoordinator Instance = new DialogCoordinator();
attraverso la suddetta proprietà allegata (il context
è il modello di vista)
var association = DialogParticipation.GetAssociation(context);
e visualizza la finestra di dialogo, invocando il metodo appropriato nella vista recuperata: questo è il modo in cui, se hai più windows aperte, la finestra di dialogo verrà visualizzata nella finestra corretta.