A better way to implement the View/Presenter pattern in CAB
The current implementation of the MVP(Model View Presenter) pattern in CAB have several issues that I don't like.
Note: In order for the proposed solution to work, you must first change the behavior of ObjectBuilder by adding the ComponentStrategy, for more info read my previous article about that: https://blogs.msdn.com/oscar_calvo/pages/a-generic-light-weight-workspace-for-cab.aspx
- You always have to manually assign the View to the Presenter and always add a [CreateNew] setter property to your View class:
[CreateNew]
public MyPresenter Presenter
{
set
{
Guard.ArgumentNotNull(value, "Presenter");
_presenter = value;
_presenter.View = this;
// I hate to remember doing this
}
get
{
return _presenter;
}
}
- You always have to add logic in your View to handle disposal of the presenters. Even worse, some people do this in the .Designer file, which is not designed to be touched by humans:
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_presenter != null)
_presenter.Dispose();
if (components != null)
components.Dispose();
}
base.Dispose(disposing);
}
- Maybe, this is the most important of all; there is no simple way to reuse the same presenter class with different classes of views. Let’s say this you develop a really powerful Tree View control designed to be used in a MVP pattern, now you have one presenter to display nested data from a database and another presenter to display the tree of an xml file. The current implementation of the MVP pattern will force you to have two different view classes for the TreeView, once for each presenter.
My idea to resolve these 3 problems is really simple, I created a “PresenterProvider” which is nothing more than a Win Forms extender provider that basically tells a control which is its presenter class.
The presenter provider is smart enough to either use object builder to build the presenter or invoke the constructor directly if the parameters to the construtor are defined in the design properties of the View. Also if the presenter implements this interface:
public interface IPresenter
{
TView View { get; set; }
}
Then is will automatically connect the View to the presenter. Here we resolve problem number one.
Problem number two is already resolved by the PresenterProvider class. When the view is been disposed then the all components in the View are also disposed. This is becuase when added the ComponentStrategy to the object builder.
And how to resolve problem number 3?
Well, easy, just add as many views of the same type as you want and connect each instance with a diferent presenter type using the property browser in the Windows Form designer.
The source code:
Here is the listing for the PresenterProvider component:
[ProvideProperty("PresenterBuildInfo", typeof(Control))]
public partial class PresenterProvider : Component, IExtenderProvider, ISupportInitialize, IDisposable
{
Dictionary<object, PresenterBuildInfo> _presentersBuildInfo;
Dictionary<object, object> _presenters;
WorkItem _workItem;
bool _isInited;
[ServiceDependency]
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public WorkItem WorkItem
{
get
{
return _workItem;
}
set
{
_workItem = value;
EndInit();
}
}
public PresenterProvider()
{
this._workItem = null;
this._presentersBuildInfo = new Dictionary<object, PresenterBuildInfo>();
this._presenters = new Dictionary<object, object>();
InitializeComponent();
}
public PresenterProvider(IContainer container)
: this()
{
container.Add(this);
}
#region IExtenderProvider Members
public bool CanExtend(object extendee)
{
if (extendee == null)
{
throw new ArgumentNullException("extendee");
}
if (extendee is Control)
{
return true;
}
return false;
}
#endregion
#region Windows Forms Designer required methods
[DefaultValue(typeof(PresenterBuildInfo), "")]
public PresenterBuildInfo GetPresenterBuildInfo(Control view)
{
return this[view];
}
public void SetPresenterBuildInfo(Control view, PresenterBuildInfo buildInfo)
{
this[view] = buildInfo;
}
#endregion
public PresenterBuildInfo this[object view]
{
set
{
if (view == null)
{
throw new ArgumentNullException("view");
}
if (!_presentersBuildInfo.ContainsKey(view))
{
_presentersBuildInfo.Add(view, value);
}
else
{
_presentersBuildInfo[view] = value;
}
}
get
{
if (view == null)
{
throw new ArgumentNullException("view");
}
if (!_presentersBuildInfo.ContainsKey(view))
{
return PresenterBuildInfo.None;
}
return _presentersBuildInfo[view];
}
}
public TPresenter Get(Control view)
{
return (TPresenter)(this._presenters[view]);
}
#region ISupportInitialize Members
private void CreatePresenters()
{
foreach (KeyValuePair<object, PresenterBuildInfo> keyValuePair in this._presentersBuildInfo)
{
Control view = keyValuePair.Key as Control;
PresenterBuildInfo presenterBuildInfo = keyValuePair.Value;
if (view != null)
{
object presenter = null;
if ( presenterBuildInfo.ContainsConstructorData )
{
presenter = presenterBuildInfo.CreateInstance();
this.WorkItem.Items.Add(presenter);
}
else
{
presenter = this.WorkItem.Items.AddNew(presenterBuildInfo.Type);
}
Type presenterInterface = typeof(IPresenter<>);
foreach(Type viewInterface in view.GetType().GetInterfaces())
{
Type concretePresenterInterface = presenterInterface.MakeGenericType(viewInterface);
if (concretePresenterInterface.IsAssignableFrom(presenter.GetType()))
{
PropertyInfo viewProperty = concretePresenterInterface.GetProperty("View");
viewProperty.SetValue(presenter, view,null);
break;
}
}
this._presenters.Add(view, presenter);
}
else
{
Type presenterType = keyValuePair.Value.Type;
throw new InvalidCastException(
String.Format(
"View associsted with presenter {0} is not an {1}",
presenterType.Name, typeof(Control).FullName));
}
}
_isInited = false;
}
public void BeginInit()
{
_isInited = false;
}
public void EndInit()
{
if (this.DesignMode || _isInited)
{
return;
}
if (this.WorkItem != null)
{
CreatePresenters();
}
}
#endregion
#region IDisposable Members
void IDisposable.Dispose()
{
foreach(KeyValuePair<object,object> keyValuePair in _presenters)
{
object presenter = keyValuePair.Value;
if (presenter != null)
{
this.WorkItem.Items.Remove(presenter);
if (presenter is IDisposable)
{
((IDisposable)presenter).Dispose();
}
presenter = null;
}
}
_presenters.Clear();
}
#endregion
}
To add designer support, you need to create the PresenterBuildInfoConverter class:
public class PresenterBuildInfoConverter : TypeConverter
{
private class ConstructorParamaterDescriptor : SimplePropertyDescriptor
{
int index;
PresenterBuildInfo buildInfo;
public ConstructorParamaterDescriptor(int index,PresenterBuildInfo buildInfo,Type parameterType, string name)
: base(buildInfo.Type, name, parameterType)
{
this.buildInfo = buildInfo;
this.index = index;
}
public override object GetValue(object component)
{
return buildInfo.ConstructorParameters[index];
}
public override void SetValue(object component, object value)
{
buildInfo.ConstructorParameters[index] = value;
}
}
private class PresenterTypePropertyDescriptor: SimplePropertyDescriptor
{
PresenterBuildInfo buildInfo;
public PresenterTypePropertyDescriptor(PresenterBuildInfo buildInfo):base(typeof(PresenterBuildInfo),"Type",typeof(Type))
{
this.buildInfo = buildInfo;
}
public override object GetValue(object component)
{
return buildInfo.Type;
}
public override void SetValue(object component, object value)
{
buildInfo.Type = (Type)value;
}
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(InstanceDescriptor))
{
return true;
}
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string typeName = value as string;
if (string.IsNullOrEmpty(typeName))
{
return PresenterBuildInfo.None;
}
Type presenterType = Type.GetType(typeName, false);
if (presenterType==null && context != null)
{
ITypeResolutionService typeResolutionService =
(ITypeResolutionService)context.GetService(typeof(ITypeResolutionService));
if (typeResolutionService != null)
{
presenterType = typeResolutionService.GetType(typeName, false);
}
}
if (presenterType != null)
{
return new PresenterBuildInfo(presenterType, null);
}
throw new ArgumentException("TextParseFailedFormat", "value");
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == null)
{
throw new ArgumentNullException("destinationType");
}
if (value is PresenterBuildInfo)
{
if (destinationType == typeof(string))
{
return value.ToString();
}
if (destinationType == typeof(InstanceDescriptor))
{
PresenterBuildInfo presenterBuildInfo = (PresenterBuildInfo)value;
if (presenterBuildInfo.Equals(PresenterBuildInfo.None))
{
PropertyInfo noneProp = typeof(PresenterBuildInfo).GetProperty("None");
return new InstanceDescriptor(noneProp, null);
}
else if (presenterBuildInfo.ContainsConstructorData )
{
Type[] typeArray = new Type[2] { typeof(Type), typeof(object[]) };
ConstructorInfo info = typeof(PresenterBuildInfo).GetConstructor(typeArray);
if (info != null)
{
object[] objArray = new object[2] { presenterBuildInfo.Type, presenterBuildInfo.ConstructorParameters };
return new InstanceDescriptor(info, objArray);
}
}
else
{
ConstructorInfo info = typeof(PresenterBuildInfo).GetConstructor(new Type[] { typeof(Type) });
if (info != null)
{
return new InstanceDescriptor(info, new object[] { presenterBuildInfo.Type });
}
}
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
public override object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues)
{
if (propertyValues == null)
{
throw new ArgumentNullException("propertyValues");
}
object objType = propertyValues["Type"];
if ( (objType == null) || (!(objType is Type)) )
{
throw new ArgumentException("PropertyValueInvalidEntry");
}
PresenterBuildInfo presenterBuildInfo = new PresenterBuildInfo((Type)objType);
ParameterInfo[] parameters=presenterBuildInfo.ConstructorInfo.GetParameters();
for(int i=0;i<parameters.Length;i++)
{
if (propertyValues.Contains(parameters[i].Name))
{
presenterBuildInfo.ConstructorParameters[i] = propertyValues[parameters[i].Name];
}
}
return presenterBuildInfo;
}
public override bool GetCreateInstanceSupported(ITypeDescriptorContext context)
{
return true;
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
PropertyDescriptorCollection collection =
new PropertyDescriptorCollection(null);
if (value != null && value is PresenterBuildInfo)
{
PresenterBuildInfo buildInfo = (PresenterBuildInfo)value;
collection.Add(new PresenterTypePropertyDescriptor(buildInfo));
if (buildInfo.ConstructorInfo!=null)
{
ParameterInfo[] parameters = buildInfo.ConstructorInfo.GetParameters();
for(int i=0;i<parameters.Length;i++)
{
ParameterInfo parameter = parameters[i];
collection.Add(
new ConstructorParamaterDescriptor(
i,
buildInfo,
parameter.ParameterType,
parameter.Name));
}
}
}
return collection;
}
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
return true;
}
}
And finally, the struct PresenterBuildInfo:
[TypeConverter(typeof(PresenterBuildInfoConverter))]
public struct PresenterBuildInfo
{
public static readonly PresenterBuildInfo None = new PresenterBuildInfo();
public PresenterBuildInfo(Type type, object[] constructorParams)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
this.type = type;
this.constructorInfo = null;
this.constructorParams = constructorParams;
Init();
}
public PresenterBuildInfo(Type type)
: this(type, null)
{
}
private Type type;
private void Init()
{
this.constructorInfo = type.GetConstructor(Type.EmptyTypes);
foreach (ConstructorInfo consInfo in type.GetConstructors())
{
if (Attribute.GetCustomAttribute(consInfo, typeof(InjectionConstructorAttribute), false) != null)
{
this.constructorInfo = consInfo;
}
}
if (this.constructorParams == null)
{
this.constructorParams = new object[this.constructorInfo.GetParameters().Length];
}
}
[Browsable(true)]
public Type Type
{
get
{
return type;
}
set
{
type = value;
Init();
}
}
private object[] constructorParams;
[Browsable(false)]
public object[] ConstructorParameters
{
get { return constructorParams; }
set { constructorParams = value; }
}
private ConstructorInfo constructorInfo;
[Browsable(false)]
public ConstructorInfo ConstructorInfo
{
get
{
return constructorInfo;
}
}
internal object CreateInstance()
{
if (this.ConstructorInfo != null)
{
return this.ConstructorInfo.Invoke(this.ConstructorParameters);
}
return null;
}
public override string ToString()
{
if (this.type == null)
{
return string.Empty;
}
return this.type.FullName;
}
public bool ContainsConstructorData
{
get
{
if (this.ConstructorParameters != null)
{
foreach (object value in this.ConstructorParameters)
{
if (value == null)
{
return false;
}
}
return true;
}
return false;
}
}
}
Let me know what you think?
Do you agree with me, is this a better implementation of the MVP pattern?
Regards,
Oscar
Comments
- Anonymous
June 09, 2009
PingBack from http://greenteafatburner.info/story.php?id=4579