tags:

views:

238

answers:

3

Hi. I'd like to build my business application using the MVVM pattern. I choose MVVM-Light because it fits to my needs. In every example I've seen about MVVM-Light, no-one use the WCF RIA. The classic MIX10 example uses a Service in the same project while WCF RIA create a service in the Web project. The question is: it looks very hard to build an interface of the whole DomainContex the WCF Ria creates (it is surely hard for me!) but without an interface how could I build a fake DomainContex to be used in Blend and in tests as well? Am I missing something? Thanks.

A: 

What I have found that works well for me is the following (which I have taken parts from both MVVM light and the RIA business template).

I build a new ViewModelBase class, inheriting from MVVM Light's ViewModelBase class, where I implementment a DomainContext, list of possibly pending operations, current operation, IsBusy property, a SaveCommand, and a protected method to log any operations created by ViewModels that inherit from this class.

Here is an example:

public class VMBase : ViewModelBase
{
    protected DomainContext _context;
    protected IList<OperationBase> operations = new List<OperationBase>();
    protected OperationBase currentOperation;

    public VMBase()
    {
        if (IsInDesignMode == false)
        {
            _context = new BudgetContext();
            _context.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_context_PropertyChanged);
        }
        SaveChangesCommand = new RelayCommand(
                () =>
                {
                    currentOperation = _context.SubmitChanges((so) =>
                    {
                        DispatcherHelper.CheckBeginInvokeOnUI(() =>
                        {
                            OnSaveComplete();
                            SaveChangesCommand.RaiseCanExecuteChanged();
                            CancelChangesCommand.RaiseCanExecuteChanged();
                        });
                    },
                        null);
                    logCurrentOperation();
                },
                () =>
                {
                    return _context != null && _context.HasChanges;
                });
        CancelChangesCommand = new RelayCommand(
                () =>
                {
                    _context.RejectChanges();
                    SaveChangesCommand.RaiseCanExecuteChanged();
                    CancelChangesCommand.RaiseCanExecuteChanged();
                },
                () =>
                {
                    return _context != null && _context.HasChanges;
                });
    }

    /// <summary>
    /// This is called after Save is Completed
    /// </summary>
    protected virtual void OnSaveComplete() { }

    void _context_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "HasChanges")
        {
            DispatcherHelper.CheckBeginInvokeOnUI(() =>
            {
                SaveChangesCommand.RaiseCanExecuteChanged();
                CancelChangesCommand.RaiseCanExecuteChanged();
            });
        }
    }

    /// <summary>
    /// Bind to Busy Indicator to show when async server
    /// call is being made to submit or load data via the
    /// DomainContext/
    /// </summary>
    public bool IsWorking
    {
        get
        {
            if (currentOperation != null)
                return !currentOperation.IsComplete;
            else
            {
                return operations.Any(o => o.IsComplete == false);
            }
        }
    }

    /// <summary>
    /// Call this command to save all changes
    /// </summary>
    public RelayCommand SaveChangesCommand { get; private set; }
    /// <summary>
    /// Revert all changes not submitted
    /// </summary>
    public RelayCommand CancelChangesCommand { get; private set; }

    /// <summary>
    /// Occurs after each operation is completed, which was registered with logCurrentOperation()
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    protected void currentOperation_Completed(object sender, EventArgs e)
    {
        currentOperation = null;
        operations.Remove((OperationBase)sender);
        DispatcherHelper.CheckBeginInvokeOnUI(() =>
        {
            RaisePropertyChanged("IsWorking");
        });
    }

    /// <summary>
    /// Logs and notifies IsBusy of the Current Operation
    /// </summary>
    protected void logCurrentOperation()
    {
        currentOperation.Completed += new EventHandler(currentOperation_Completed);
        operations.Add(currentOperation);
        RaisePropertyChanged("IsWorking");
    }

    /// <summary>
    /// Just make sure any async calls are done
    /// </summary>
    public override void Cleanup()
    {
        // Clean own resources if needed
        foreach (OperationBase op in operations)
        {
            if (op.IsComplete == false)
            {
                if (op.CanCancel)
                    op.Cancel();
            }
        }

        base.Cleanup();
    }
}

Then, in your actual view models, you can focus on the properties, and commands for the view model - and all the domain context is really wired for you already. Just use the _context property - for example:

void loadFolderInfo()
    {
        if (_context != null)
        {
            EntityQuery<eFolder> query = _context.GetEFoldersByFolderQuery(_efolderid);
            currentOperation =
                _context.Load<eFolder>(query,
                new Action<LoadOperation<eFolder>>((lo) =>
                    {
                        if (lo.HasError)
                        {
                            Messenger.Default.Send<DialogMessage>(
                                new DialogMessage(lo.Error.Message, (mbr) =>
                                    {}));
                        }
                        DispatcherHelper.CheckBeginInvokeOnUI(
                            () =>
                            {
                                eFolder myFolder = lo.Entities.First();
                                Subject = myFolder.eSubject;
                                FolderName = myFolder.eFolderName;


                            });

                    }), null);
            logCurrentOperation();
        }
    }

and a property might look something like this:

public string EFOLDERID
    {
        get
        {
            return _efolderid;
        }

        set
        {
            if (_efolderid == value)
            {
                return;
            }

            var oldValue = _efolderid;
            _efolderid = value;

            // Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
            RaisePropertyChanged("EFOLDERID", oldValue, value, true);
            loadFolderInfo();
        }
    }

The key part is that the VMBase class handles all wiring and management of the DomainContext. To make this happen, in your ViewModel implementations, be sure to assign any _context.BaseOperationCall(..)'s return value to the currrentOperation, then immediately call the logCurrentOperation. After that, it's hands off. You can then bind a BusyIndicator to your IsWorking propery, and you have a simplified RIA implementation.

Hopefully this helps you get started.

Ryan from Denver
+2  A: 

Thank Ryan for your answer. I write as an answer because I registered only after this question.

Your VM is a very good start but I still have one. Anyway you VM has some very good ideas I'd like to implement.

The general problem is this kind of VM is not blendable because it's not possible to create an Interface (or I'm not able to do it) of the context so to build a fake context itself with fake data to bind with Expression Blend. Laurent, in the MIX10 example, use a very simple web service with 2 methods if I well remeber. Of course build an interface with 2 methods and create a fake context is easier than a context with several methods.

EDIT Jul 21 18.08GMT+1 Ryan, you last comment about the ViewModelLocator make me think. The problem was Blend keep telling me that "the resource 'Locator' was not found" (or something like that). In fact I declared the static resource only in app.xaml. I soppose Blend dosen't care very much about a resource declared in app.xaml when you are editing another user control. So I declared the same resource in the control itself and BUUM! I had the view bound with the VM finally! Thanks for your support. I'm sorry I can't mark your answer as a solution for the account problem I've said.

Angelo Chiello
How about a vote up on my answer at least if you can't mark it. Thanks, Ryan
Ryan from Denver
+2  A: 

Actually, the solution I am using is blendable, and makes the blending process even simpler.

Here is an exmample of how I accomplish this using this framework:

 public class FolderViewModel : VMBase
{
    private string _subject = string.Empty;
    private string _folderName = string.Empty;
    private string _service = string.Empty;
    private string _dept = string.Empty;
    private string _efolderid = string.Empty;
    private string _timingName = string.Empty;
    private WorkplanBase _planBase = null;
    private IEnumerable<PSCustomList> _timingOptions = null;
    private decimal _totalvalue = 0;


    public FolderViewModel()
    {
        registerForMessages();

        if (IsInDesignMode)
        {
            // Code runs in Blend --> create design time data.
            EFOLDERID = "0123456790123456790123456791";
            Subject = "9999-00 - This is a test nVision Subject";
            Service = "AUDCOMP";
            Department = "AUDIT";
            FolderName = "NVSN003000";


            List<PSCustomList> listItems = new List<PSCustomList>();
            listItems.Add(new PSCustomList()
            {
                ID = "1234",
                ParameterValue = "Busy Season"
            });
            listItems.Add(new PSCustomList()
            {
                ID = "1134",
                ParameterValue = "Another Season"
            });

            _timingOptions = listItems.ToArray();
            _totalvalue = 12000;

            PlanBase = new WorkplanBase()
            {
                ClientFee = 15000,
                Timing = "1234"
            };
        }
    }
}

Then all the sample data is defined in the constructor of the Acutal View Models that are bound in your ViewModelLocator class. The VMBase takes care of not trying to instantiate the DataContext when you are in blend.

Ryan from Denver