5
\$\begingroup\$

Having a view (grid control) that provides some functionallity via methods (primary SerializeView / DeserializeView) that should be called from the view model which is bound to the view.

One point to consinder is, that the view model may not be bound to the view during the deserialization process. Therefore, the serialization state of the view must be buffered in the view model and applied when the view model will be bound to the view.

The following solution can handle such use cases in general:

Implementation

IViewAwareViewModel interface that must be implemented by the view model:

public interface IViewAwareViewModel<TView> where TView : class
{
    ViewConnection<TView> ViewConnection { get; }
}

ViewConection that encapsulates the view and provides "delayed access".

public class ViewConnection<TView> where TView : class
{
    private TaskCompletionSource<TView> taskCompletionSource = new TaskCompletionSource<TView>();

    public Task<TView> View => this.taskCompletionSource.Task;

    public bool IsConnected => this.taskCompletionSource.Task.IsCompleted;

    public void Release()
    {
        this.taskCompletionSource = new TaskCompletionSource<TView>();
    }

    public void RunOnView(Action<TView> action)
    {
        this.View.ContinueWith(task => action(task.Result));
    }

    internal void SetView(TView view)
    {
        if (this.IsConnected)
        {
            throw new InvalidOperationException("Unable to connect to a view twice.");
        }
        this.taskCompletionSource.SetResult(view);
    }
}

ViewConnector that provides a helper method for connecting the view with the view model.

public static class ViewConnector<TView> where TView : class
{
    public static void Register(FrameworkElement target)
    {
        if (target == null)
        {
            throw new ArgumentNullException(nameof(target));
        }

        if (!typeof(TView).IsInstanceOfType(target))
        {
            throw new InvalidOperationException($"Object of type '{target.GetType()}' is not compatible to type '{typeof(TView)}'.");
        }

        target.DataContextChanged += TargetDataContextChanged;
    }

    private static void TargetDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var oldViewModel = e.OldValue as IViewAwareViewModel<TView>;
        var newViewModel = e.NewValue as IViewAwareViewModel<TView>;

        if (oldViewModel != null)
        {
            oldViewModel.ViewConnection?.Release();
        }

        if (newViewModel != null)
        {
            newViewModel.ViewConnection.SetView(sender as TView);
        }
    }
}

Usage

View Registration

var dataGrid = new MyDataGrid(); // implements: IDataGridAbstractionForViewModel
ViewConnector<IDataGridAbstractionForViewModel>.Register(dataGrid);

ViewModel Implementaion

public class DataGridViewModel : ViewModelBase, IViewAwareViewModel<IDataGridAbstractionForViewModel>
{
    public ViewConnection<IDataGridAbstractionForViewModel> ViewConnection { get; } = new ViewConnection<IDataGridAbstractionForViewModel>();

    protected override string SerializeState()
    {
        if (this.ViewConnector.IsConnected)
        {
            var view = this.ViewConnection.View.Result;
            return view.SaveLayout();
        }

        return string.Empty;
    }

    protected override void DeserializeState(string state)
    {
        base.DeserializeState(state);

        if (string.IsNullOrEmpty(state))
        {
            return;
        }

        this.ViewConnection.RunOnView(view => view.RestoreLayout(state));

    }

    protected override void OnRemoved()
    {
        base.OnRemoved();

        this.ViewConnection.Release();
    }
}

Questions

  • Are there other cleaner solutions / approaches?
  • Do you see any risk of memory leaks?
  • General feedback? :)
\$\endgroup\$
  • \$\begingroup\$ What happened to the my prefix?! ;-) \$\endgroup\$ – t3chb0t Aug 8 '18 at 14:18
  • 1
    \$\begingroup\$ @t3chb0t: I have changed the company and with the company the coding conventions have also changed ;) \$\endgroup\$ – JanDotNet Aug 8 '18 at 18:27
  • 1
    \$\begingroup\$ Yuhuu, cured! ;-D \$\endgroup\$ – t3chb0t Aug 8 '18 at 18:32
3
\$\begingroup\$

ViewConnection class looks cool, I think that's a clever use of tasks. But frankly I don't see why you can't have a

public string State { get {...} set {...}}

property on both view and viewmodel and solve your problem with two-way databinding. You can set this property on your viewmodel when you load your layout. If view is there - fine, it will be notified that the property has changed and you can handle this change on the view side (i.e. update datagrid with new state). If the view is not there - also fine, it will fetch the property from viewmodel as soon as you set its DataContext. In similar fashion you can save datagrid state on view side, set View.State and handle the new state in ViewModel.State property (i.e save it to file). You do not show your view class, but I don't see any problems with this approach off the bat, and I think that's how those things should be done normally.

\$\endgroup\$
  • \$\begingroup\$ Indeed, a state property on view and view model should work for the case "deserialize". Doing it that way would make the ViewConnection class superfluous - which is an advantage :). However, serializing the whole grid is an expensive process that should only happen when the grid's state is requested by the view model. Therefore it is (IMHO) still required to access the view by the view model. \$\endgroup\$ – JanDotNet Aug 8 '18 at 13:12
  • \$\begingroup\$ @JanDotNet that depends on the use-case. If user initializes the serialization by, say, pressing a "save" button or by closing the window, then it makes sense to trigger serialization on view side. If however serialization is triggered programmatically by some business logic on viewmodel side, then some additional link between view and view model is indeed required. In those cases I often feel like it is better to leak viewmodel to view (and subscribe view to viewmodel events), but you approach is fine as well. \$\endgroup\$ – Nikita B Aug 8 '18 at 13:34

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.