2
\$\begingroup\$

I am currently working on a project which uses the MVVM pattern. I have a POCO which I want to present in an editor and, because the object is composed from a number of smaller, reusable POCOs, I thought it would be appropriate to show it in a TabControl.

The result looks like this:

Picture=1k words

On the left is a collection of ProductViewModel and on the right is an instance of ProductEditorViewModel.

The editor splits the Product properties into groups - all of which have one or more View/ViewModel pairs. Each ViewModel has an IsDirty property to indicate its' edited status and an IsEditable property which defines whether the content can be edited or not.

What I am finding that it is tricky to get all the ViewModels properly linked together so that the IsEditable status gets passed down into nested ViewModels and that the IsDirty property gets passed up so that, for example, the Save button can be enabled.

I'm trying to keep it all as loose as possible. In the right of the picture is a list of Events with associated Actions. The selected MessageAction (see screen-shot) can itself define lists of Actions which are editable (in a separate dialog window). It can get quite complicated!

Any suggestions appreciated.

Here are some snippets:

At top level

    #region COMMAND - EditProductCommand
    private ICommand _EditProductCommand;
    public ICommand EditProductCommand
    {
        get
        {
            if (this._EditProductCommand == null)
            {
                this._EditProductCommand = new RelayCommand(parm => DoEditProductCommand(), parm => CanDoEditProductCommand());
            }
            return this._EditProductCommand;
        }
    }

    private bool CanDoEditProductCommand()
    {
        // can only edit if editor, owner of product and editor not already in use
        return (app.CurrentUser.HasRole(Role.ProductEditor)) 
            && SelectedProduct != null
            && ProductEditorVM.IsDirty == false 
            && (SelectedProduct.Product.CheckedOutBy == null    // not checked out
            || SelectedProduct.Product.CheckedOutBy.UserGuid == app.CurrentUser.UserGuid);  // or owner
    }

    private void DoEditProductCommand()
    {
        if (SelectedProduct != null && SelectedProduct.IsCheckedOut == false)
        {
            CheckoutCommand.Execute(null);
        }
        ProductEditorVM.EditProduct = SelectedProduct.Product;
        ProductEditorVM.IsEditable = true;
    }
    #endregion

ProductEditorView

    <TabControl Grid.Row="0">
        <TabItem Header="{x:Static me:Messages.Tab_Product_Core}">
            <ContentControl Content="{Binding CoreEditorVM}" IsEnabled="{Binding IsEditable}"/>
        </TabItem>
        <TabItem Header="{x:Static me:Messages.Tab_Product_Events}">
            <ContentControl Content="{Binding EventTreeVM}" 
                            IsEnabled="{Binding IsEditable}"
                            HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
        </TabItem>

ProductEditorViewModel

public class ProductEditorViewModel : ViewModelBase
{
    private IApplicationManager app;
    private static log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

    #region PROPERTIES - SELECTED PRODUCT
    private IProduct _EditProduct;
    public IProduct EditProduct
    {
        get { return _EditProduct; }
        set
        {
            if (value != _EditProduct)
            {
                _EditProduct = value;
                OnPropertyChanged("EditProduct");
                InitialiseProduct(_EditProduct);
            }
        }
    }
    #endregion

    #region PROPERTIES - CORE
    private ProductCoreEditorViewModel _CoreEditorVM;
    public ProductCoreEditorViewModel CoreEditorVM
    {
        get { return _CoreEditorVM; }
        set
        {
            if (value != _CoreEditorVM)
            {
                _CoreEditorVM = value;
                OnPropertyChanged("CoreEditorVM");
            }
        }
    }
    #endregion

    [... and more ...]

    #region CONSTRUCTOR
    public ProductEditorViewModel(IApplicationManager app)
    {
        this.app = app;
        CoreEditorVM = new ProductCoreEditorViewModel();
        EventTreeVM = new ActionTreeViewModel(app);
        HistoryVM = new HistoryViewModel(app);

        CoreEditorVM.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(VM_PropertyChanged);
        EventTreeVM.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(VM_PropertyChanged);
    }


    private void InitialiseProduct(IProduct product)
    {
        CoreEditorVM.Initialise(product);
        EventTreeVM.Initialise(product.Events.EventList);
        HistoryVM.ItemsList = product == null ? null : app.PersistenceManager.GetProducts(
                new ProductCriteria(app.CurrentUser, false).Add(ProductCriteria.HasId(product.ProductGuid)),
                false);

        IsDirty = false;
    }

    void VM_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "IsDirty")
        {
            UpdateDirty();
        }
    }

    private void UpdateDirty()
    {
        IsDirty = CoreEditorVM.IsDirty || EventTreeVM.IsDirty; 
    }
    #endregion

ViewModelBase

public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
{
    bool _isDirty;
    public bool IsDirty
    {
        get { return _isDirty; }
        set
        {
            if (value != IsDirty)
            {
                _isDirty = value;
                OnPropertyChanged("IsDirty");
            }
        }
    }

    private bool _IsEditable;
    public bool IsEditable
    {
        get { return _IsEditable; }
        set
        {
            if (value != _IsEditable)
            {
                _IsEditable = value;
                OnPropertyChanged("IsEditable");
            }
        }
    }

    private bool _result;
    public bool DialogResult
    {
        get { return _result; }
        set
        {
            _result = value;
            OnPropertyChanged("DialogResult");
        }
    }

    [...and more]
\$\endgroup\$
  • \$\begingroup\$ I guess you're going to have to show a bit of code how your ViewModels or POCOs are structured. \$\endgroup\$ – Abbas Jun 26 '13 at 10:24
  • \$\begingroup\$ @Abbas - code snippets ready for you ;-) \$\endgroup\$ – paul Jun 26 '13 at 12:17
2
\$\begingroup\$

Looks pretty good. Just a few little things:

  • Command-related methods CanDoXXXX and DoXXXX would be better off named CanExecuteXXXX and ExecuteXXXX or OnExecuteXXXX.
  • I would put the ViewModel properties IsEditable and IsDirty in some IEditable interface that applicable ViewModels implement. Looks like reusable stuff.
  • Your VM is tightly coupled with your log provider - I'd wrap it with some ILogger interface which would be constructor-injected as needed.
  • You're not always consistent with access modifiers: sometimes you do specify the default (private void InitialiseProduct), other times you leave it (void VM_PropertyChanged). I'd specify it everywhere (be explicit).
  • IApplicationManager is dangerous. You're passing it as a dependency which is good, but its name (xxxxManager) suggests that it might, over time, become a shove-it-all-in-there helper class with low cohesion. You don't show much code, but if all it's used for in the VM is to get the app.CurrentUser, maybe the VM's dependencies are more with some ICurrentUser than the whole IApplicationManager.
  • Your ViewModelBase doesn't show its INotifyPropertyChanged implementation, I'd do something like this so instead of OnPropertyChanged("CoreEditorVM") you could do OnPropertyChanged(() => CoreEditorVM) and have a strongly-typed way of referring to your property names.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.