2010-12-29 7 views
14

Essendo nuovo per WPF & MVVM Ho difficoltà con alcune funzionalità di base.MVVM - implementazione della funzionalità 'IsDirty' in un ModelView per il salvataggio dei dati

vorrei prima spiegare quello che sono dopo, e quindi allego qualche esempio di codice ...

ho una schermata che mostra un elenco di utenti, e visualizzo i dettagli dell'utente selezionato sulla destra, lato con caselle di testo modificabili. Ho quindi un pulsante Salva che è DataBound, ma mi piacerebbe solo che questo pulsante venga visualizzato quando i dati sono effettivamente cambiati. cioè - ho bisogno di verificare la presenza di "dati sporchi".

Ho un esempio completamente MVVM in cui ho un modello chiamato User:

namespace Test.Model 
{ 
    class User 
    { 
     public string UserName { get; set; } 
     public string Surname { get; set; } 
     public string Firstname { get; set; } 
    } 
} 

Poi, il ViewModel si presenta così:

using System.Collections.ObjectModel; 
using System.Collections.Specialized; 
using System.Windows.Input; 
using Test.Model; 

namespace Test.ViewModel 
{ 
    class UserViewModel : ViewModelBase 
    { 
     //Private variables 
     private ObservableCollection<User> _users; 
     RelayCommand _userSave; 

     //Properties 
     public ObservableCollection<User> User 
     { 
      get 
      { 
       if (_users == null) 
       { 
        _users = new ObservableCollection<User>(); 
        //I assume I need this Handler, but I am stuggling to implement it successfully 
        //_users.CollectionChanged += HandleChange; 

        //Populate with users 
        _users.Add(new User {UserName = "Bob", Firstname="Bob", Surname="Smith"}); 
        _users.Add(new User {UserName = "Smob", Firstname="John", Surname="Davy"}); 
       } 
       return _users; 
      } 
     } 

     //Not sure what to do with this?!?! 

     //private void HandleChange(object sender, NotifyCollectionChangedEventArgs e) 
     //{ 
     // if (e.Action == NotifyCollectionChangedAction.Remove) 
     // { 
     //  foreach (TestViewModel item in e.NewItems) 
     //  { 
     //   //Removed items 
     //  } 
     // } 
     // else if (e.Action == NotifyCollectionChangedAction.Add) 
     // { 
     //  foreach (TestViewModel item in e.NewItems) 
     //  { 
     //   //Added items 
     //  } 
     // } 
     //} 

     //Commands 
     public ICommand UserSave 
     { 
      get 
      { 
       if (_userSave == null) 
       { 
        _userSave = new RelayCommand(param => this.UserSaveExecute(), param => this.UserSaveCanExecute); 
       } 
       return _userSave; 
      } 
     } 

     void UserSaveExecute() 
     { 
      //Here I will call my DataAccess to actually save the data 
     } 

     bool UserSaveCanExecute 
     { 
      get 
      { 
       //This is where I would like to know whether the currently selected item has been edited and is thus "dirty" 
       return false; 
      } 
     } 

     //constructor 
     public UserViewModel() 
     { 

     } 

    } 
} 

Il "RelayCommand" è solo un semplice involucro classe, così come "ViewModelBase". (Vi allego quest'ultimo però solo per chiarezza)

using System; 
using System.ComponentModel; 

namespace Test.ViewModel 
{ 
    public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable 
    { 
     protected ViewModelBase() 
     { 
     } 

     public event PropertyChangedEventHandler PropertyChanged; 

     protected virtual void OnPropertyChanged(string propertyName) 
     { 
      PropertyChangedEventHandler handler = this.PropertyChanged; 
      if (handler != null) 
      { 
       var e = new PropertyChangedEventArgs(propertyName); 
       handler(this, e); 
      } 
     } 

     public void Dispose() 
     { 
      this.OnDispose(); 
     } 

     protected virtual void OnDispose() 
     { 
     } 
    } 
} 

Infine - XAML

<Window x:Class="Test.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:vm="clr-namespace:Test.ViewModel" 
     Title="MainWindow" Height="350" Width="525"> 
    <Window.DataContext> 
     <vm:UserViewModel/> 
    </Window.DataContext> 
    <Grid> 
     <ListBox Height="238" HorizontalAlignment="Left" Margin="12,12,0,0" Name="listBox1" VerticalAlignment="Top" 
       Width="197" ItemsSource="{Binding Path=User}" IsSynchronizedWithCurrentItem="True"> 
      <ListBox.ItemTemplate> 
      <DataTemplate> 
       <StackPanel> 
         <TextBlock Text="{Binding Path=Firstname}"/> 
         <TextBlock Text="{Binding Path=Surname}"/> 
       </StackPanel> 
      </DataTemplate> 
      </ListBox.ItemTemplate> 
     </ListBox> 
     <Label Content="Username" Height="28" HorizontalAlignment="Left" Margin="232,16,0,0" Name="label1" VerticalAlignment="Top" /> 
     <TextBox Height="23" HorizontalAlignment="Left" Margin="323,21,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="{Binding Path=User/UserName}" /> 
     <Label Content="Surname" Height="28" HorizontalAlignment="Left" Margin="232,50,0,0" Name="label2" VerticalAlignment="Top" /> 
     <TextBox Height="23" HorizontalAlignment="Left" Margin="323,52,0,0" Name="textBox2" VerticalAlignment="Top" Width="120" Text="{Binding Path=User/Surname}" /> 
     <Label Content="Firstname" Height="28" HorizontalAlignment="Left" Margin="232,84,0,0" Name="label3" VerticalAlignment="Top" /> 
     <TextBox Height="23" HorizontalAlignment="Left" Margin="323,86,0,0" Name="textBox3" VerticalAlignment="Top" Width="120" Text="{Binding Path=User/Firstname}" /> 
     <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="368,159,0,0" Name="button1" VerticalAlignment="Top" Width="75" Command="{Binding Path=UserSave}" /> 
    </Grid> 
</Window> 

Quindi, in pratica, quando modifico un cognome, il pulsante Salva dovrebbe essere attivato; e se annullo la mia modifica - beh allora dovrebbe essere di nuovo disabilitata poiché nulla è cambiato.

Ho visto questo in molti esempi, ma non ho ancora scoperto come farlo.

Qualsiasi aiuto sarebbe molto apprezzato! Brendan

risposta

0

Poiché il comando UserSave è nel ViewModel, farei il tracciamento dello stato "sporco" lì. Vorrei fare il databind all'elemento selezionato nel ListBox e, quando cambia, memorizzare un'istantanea dei valori correnti delle proprietà dell'utente selezionato. Quindi è possibile confrontare questo per determinare se il comando deve essere abilitato/disabilitato.

Tuttavia, poiché si sta vincolando direttamente al modello, è necessario un modo per scoprire se qualcosa è cambiato. O implementate anche INotifyPropertyChanged nel modello, oppure avvolgete le proprietà in un ViewModel.

Si noti che quando CanExecute del comando cambia, potrebbe essere necessario attivare CommandManager.InvalidateRequerySuggested().

3

Se si desidera adottare un approccio di framework piuttosto che scrivere l'infrastruttura da soli, è possibile utilizzare CSLA (http://www.lhotka.net/cslanet/), il framework di Rocky per lo sviluppo di oggetti business. Lo stato dell'oggetto è gestito per te sulle modifiche alle proprietà e il codice base include anche un esempio di tipo ViewModel che supporta un modello sottostante, un verbo Salva e una proprietà CanSave. Potresti essere in grado di prendere spunto dal codice, anche se non volevi usare il framework.

4

Ti suggerisco di utilizzare GalaSoft MVVM Light Toolkit in quanto è molto più facile da implementare rispetto all'approccio fai-da-te.

Per le letture dirty, è necessario mantenere l'istantanea di ciascun campo e restituire true o false dal metodo UserSaveCanExecute(), che abiliterà/disabiliterà il pulsante di comando di conseguenza.

+0

Vorrei utilizzare anche MVVM Light Toolkit –

+3

Grazie, ho installato il toolkit MVVM-Light, ma non ho visto alcun modo di implementare facilmente la funzionalità "IsDirty". Sono comunque riuscito a risolvere il mio problema (forse non nel modo migliore - ma funziona) - risponderà alla mia domanda un po 'più tardi con i dettagli – Brendan

+0

MVVM non supporta la funzionalità di lettura sporca. Era solo un suggerimento per ridurre al minimo lo sforzo per l'implementazione del pattern MVVM. È bello sapere che hai già letto la lettura sporca. Ora suggerirei di esplorare ulteriori messaggi e eventi da comandare in light toolkit che ti daranno maggiore controllo. Divertiti. – ShahidAzim

7

In base alla mia esperienza, se si implementa IsDirty nel modello di visualizzazione, probabilmente si desidera anche che il modello di visualizzazione implementa IEditableObject.

Supponendo che il vostro modello di vista è la solita, implementazione PropertyChanged e un privato o protetto OnPropertyChanged metodo che solleva, impostazione IsDirty è abbastanza semplice: è sufficiente impostare IsDirty in OnPropertyChanged se non è già vero.

Il tuo setter IsDirty dovrebbe, se la proprietà era falsa e ora è true, chiamare BeginEdit.

Il comando Save deve chiamare EndEdit, che aggiorna il modello di dati e imposta IsDirty su falso.

Il comando Cancel deve chiamare CancelEdit, che aggiorna il modello di visualizzazione dal modello dati e imposta IsDirty su falso.

I CanSave e CanCancel proprietà (supponendo che si sta utilizzando un RelayCommand per questi comandi) solo restituire il valore corrente di IsDirty.

Si noti che poiché nessuna di queste funzionalità dipende dall'implementazione specifica del modello di vista, è possibile inserirla in una classe base astratta. Le classi derivate non devono implementare nessuna delle proprietà relative ai comandi o la proprietà IsDirty; devono semplicemente ignorare BeginEdit, EndEdit e CancelEdit.

0

Ecco come ho implementato IsDirty. Crea un wrapper per ogni proprietà della classe User (ereditando la classe User con IPropertyChanged e implementando onpropertychanged in User class wont help) nel tuo ViewModal. È necessario modificare l'associazione da UserName a WrapUserName.

public string WrapUserName 
    { 
     get 
     { 
      return User.UserName   
     } 
     set 
     { 
      User.UserName = value; 
      OnPropertyChanged("WrapUserName"); 
     } 
    } 

Ora hanno una proprietà

public bool isPageDirty 
    { 
     get; 
     set; 
    }  

Dal momento che il viewmodal eredita da baseviewmodal e implementa baseviewmodal OnPropertyChanged.

UserViewModel.PropertyChanged += (s, e) => { isPageDirty = true; };  

Nel caso in cui uno qualsiasi dei propertychanges, isPageDirty sarà vero, Così, mentre risparmiando chan controllare isPageDirty.

+0

Grazie. Questo sembra interessante - giocherò con esso stasera. Tuttavia, ho trovato un'altra soluzione che spiegherò di seguito un po 'più tardi – Brendan

2

Ho trovato una soluzione funzionante. Questo ovviamente non può essere il modo migliore, ma sono sicuro che posso lavorarci mentre imparo di più ...

Quando eseguo il progetto, se ricopro qualsiasi elemento, la casella di riepilogo è disabilitata e pulsante di salvataggio abilitato. Se annullo le mie modifiche, la casella di riepilogo è abilitata di nuovo e il pulsante Salva è disabilitato.

ho cambiato il mio modello d'uso per implementare INotifyPropertyChanged, e ho anche creato una serie di variabili private per memorizzare i "valori originari" e un po 'di logica per verificare la presenza di "IsDirty"

using System.ComponentModel; 
namespace Test.Model 
{ 
    public class User : INotifyPropertyChanged 
    { 
    //Private variables 
    private string _username; 
    private string _surname; 
    private string _firstname; 

    //Private - original holders 
    private string _username_Orig; 
    private string _surname_Orig; 
    private string _firstname_Orig; 
    private bool _isDirty; 

    //Properties 
    public string UserName 
    { 
     get 
     { 
      return _username; 
     } 
     set 
     { 
      if (_username_Orig == null) 
      { 
       _username_Orig = value; 
      } 
      _username = value; 
      SetDirty(); 
     } 
    } 
    public string Surname 
    { 
     get { return _surname; } 
     set 
     { 
      if (_surname_Orig == null) 
      { 
       _surname_Orig = value; 
      } 
      _surname = value; 
      SetDirty(); 
     } 
    } 
    public string Firstname 
    { 
     get { return _firstname; } 
     set 
     { 
      if (_firstname_Orig == null) 
      { 
       _firstname_Orig = value; 
      } 
      _firstname = value; 
      SetDirty(); 
     } 
    } 

    public bool IsDirty 
    { 
     get 
     { 
      return _isDirty; 
     } 
    } 

    public void SetToClean() 
    { 
     _username_Orig = _username; 
     _surname_Orig = _surname; 
     _firstname_Orig = _firstname; 
     _isDirty = false; 
     OnPropertyChanged("IsDirty"); 
    } 

    private void SetDirty() 
    { 
     if (_username == _username_Orig && _surname == _surname_Orig && _firstname == _firstname_Orig) 
     { 
      if (_isDirty) 
      { 
       _isDirty = false; 
       OnPropertyChanged("IsDirty"); 
      } 
     } 
     else 
     { 
      if (!_isDirty) 
      { 
       _isDirty = true; 
       OnPropertyChanged("IsDirty"); 
      } 
     } 
    } 

    public event PropertyChangedEventHandler PropertyChanged; 

    protected void OnPropertyChanged(string propertyName) 
    { 
     PropertyChangedEventHandler handler = PropertyChanged; 

     if (handler != null) 
     { 
      handler(this, new PropertyChangedEventArgs(propertyName)); 
     } 
    } 
} 

Poi , anche il mio ViewModel è cambiato un po '....

using System.Collections.ObjectModel; 
using System.Collections.Specialized; 
using System.Windows.Input; 
using Test.Model; 
using System.ComponentModel; 

namespace Test.ViewModel 
{ 
    class UserViewModel : ViewModelBase 
    { 
     //Private variables 

    private ObservableCollection<User> _users; 
    RelayCommand _userSave; 
    private User _selectedUser = new User(); 

    //Properties 
    public ObservableCollection<User> User 
    { 
     get 
     { 
      if (_users == null) 
      { 
       _users = new ObservableCollection<User>(); 
       _users.CollectionChanged += (s, e) => 
       { 
        if (e.Action == NotifyCollectionChangedAction.Add) 
        { 
         // handle property changing 
         foreach (User item in e.NewItems) 
         { 
          ((INotifyPropertyChanged)item).PropertyChanged += (s1, e1) => 
           { 
            OnPropertyChanged("EnableListBox"); 
           }; 
         } 
        } 
       }; 
       //Populate with users 
       _users.Add(new User {UserName = "Bob", Firstname="Bob", Surname="Smith"}); 
       _users.Add(new User {UserName = "Smob", Firstname="John", Surname="Davy"}); 
      } 
      return _users; 
     } 
    } 

    public User SelectedUser 
    { 
     get { return _selectedUser; } 
     set { _selectedUser = value; } 
    } 

    public bool EnableListBox 
    { 
     get { return !_selectedUser.IsDirty; } 
    } 

    //Commands 
    public ICommand UserSave 
    { 
     get 
     { 
      if (_userSave == null) 
      { 
       _userSave = new RelayCommand(param => this.UserSaveExecute(), param => this.UserSaveCanExecute); 
      } 
      return _userSave; 
     } 
    } 

    void UserSaveExecute() 
    { 
     //Here I will call my DataAccess to actually save the data 
     //Save code... 
     _selectedUser.SetToClean(); 
     OnPropertyChanged("EnableListBox"); 
    } 

    bool UserSaveCanExecute 
    { 
     get 
     { 
      return _selectedUser.IsDirty; 
     } 
    } 

    //constructor 
    public UserViewModel() 
    { 

    } 

} 

Infine, il codice XAML ho cambiato le associazioni sul nome utente, cognome & Nome includere UpdateSourceTrigger=PropertyChanged E poi ho legato della casella di riepilogo SelectedItem e IsEnabled

Come ho detto all'inizio - non può essere la soluzione migliore, ma sembra funzionare ...

3

Ho lavorato sull'implementazione di IsDirty per i modelli avvolti nel mio ViewModel.

Il risultato davvero semplificato le mie ViewModels:

public class PersonViewModel : ViewModelBase 
{ 
    private readonly ModelDataStore<Person> data; 
    public PersonViewModel() 
    { 
     data = new ModelDataStore<Person>(new Person()); 
    } 

    public PersonViewModel(Person person) 
    { 
     data = new ModelDataStore<Person>(person); 
    } 

    #region Properties 

    #region Name 
    public string Name 
    { 
     get { return data.Model.Name; } 
     set { data.SetPropertyAndRaisePropertyChanged("Name", value, this); } 
    } 
    #endregion 

    #region Age 
    public int Age 
    { 
     get { return data.Model.Age; } 
     set { data.SetPropertyAndRaisePropertyChanged("Age", value, this); } 
    } 
    #endregion 

    #endregion 
} 

Codice @http://wpfcontrols.codeplex.com/ controllare sotto i modelli di assemblaggio e la cartella MVVM, troverete una classe ModelDataStore.

P.S. Non ho fatto un test completo su di esso, solo il test davvero semplice che troverete il gruppo Test.

Problemi correlati