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? :)
my
prefix?! ;-) \$\endgroup\$ – t3chb0t Aug 8 '18 at 14:18