简体   繁体   中英

MVP, WinForms - how to avoid bloated view, presenter and presentation model

When implementing MVP pattern in winforms I often find bloated view interfaces with too many properties, setters and getters. An easy example with be a view with 3 buttons and 7 textboxes, all having value, enabled and visible properties exposed from the view. Adding validation results for this, and you could easily end up with an interface with 40ish properties. Using the Presentation Model, there'll be a model with the same number of properties aswell.

How do you easily sync the view and the presentation model without having bloated presenter logic that pass all the values back and forth? (With that 80ish line presenter code, imagine with the presenter test that mocks the model and view will look like..160ish lines of code just to mock that transfer.) Is there any framework to handle this without resorting to winforms databinding? (you might want to use different views than a winforms view. According to some, this sync should be the presenters job..) Would you use AutoMapper?

Maybe im asking the wrong questions, but it seems to me MVP easily gets bloated without some good solution here..

This is just one idea, and I know where some people might not like it -- there are lots of different things you can do here.

If you find yourself using a lot of boilerplate code, encapsulate it.

public class UiField<ContentType>
{
    public bool IsEnabled { get; set; }
    public ContentType Value { get; set; }
    public bool IsVisible { get; set; }
}

In your view, then:

public interface ISampleView
{
    UiField<bool> IsStaffFullTime { get; set; }
    UiField<string> StaffName { get; set; }
    UiField<string> JobTitle { get; set; }
    UiField<int> StaffAge { get; set; }
    UiField<IList<string>> Certifications { get; set; }
}

Here you wrap up the various properties associated with each field.

Incidentally, I suggest that you not stub these interfaces by hand for testing -- use a mocking framework.

I started building on Jay's solution then realised that what I was building was implementation of the Adapter Design Pattern. Here is what I ended up with, its VB.Net rather than C#:

The adapter Interface which defines the properties you only want to set up once:

Public Interface IWinformsControlAdapter(Of T)

''' <summary>
''' Default value for the UI Control
''' </summary>
''' <returns></returns>
Property Value As T
Property IsEnabled As Boolean
Sub SetError(myValue As String)

''' <summary>
''' Select all the text in the UI Field if possible
''' </summary>
Sub [Select]()

''' <summary>
''' Gets a boolean value indicating whether all required inputs have been entered into the UI field
''' </summary>
''' <returns>true if all required input has been entered in the field</returns>
ReadOnly Property Completed As Boolean

Property DataSource As Object
Property DisplayMember As String
Property ValueMember As String
Property SelectedValue As Object
Property Checked As Boolean
End Interface

The Interface Implementation:

Public Class WinformsControlAdapter(Of T)
    Implements IWinformsControlAdapter(Of T)

    Private ReadOnly _control As Control 'The Adaptee
    Private ReadOnly _errorProvider As ErrorProvider
    Public Sub New(ByRef control As Control, ByRef errorProvider As ErrorProvider)
        _control = control
        _errorProvider = errorProvider
    End Sub

    Public Property Value As T Implements IWinformsControlAdapter(Of T).Value
        Get

            Select Case True
                Case TypeOf _control Is TextBoxBase 
                    'Typically for Textbox T = String, but not always: _control could be adapting to Integer for inputting numbers or MaskedTextbox could be adapting to Date for inputting dates, etc 
                    Return ConvertTo(Of T)(_control.Text)

                Case TypeOf _control Is RadioButton 
                    Dim myRadioButton As RadioButton = _control
                    Return ConvertTo(Of T)(myRadioButton.Checked)

                Case Else
                    Throw New NotImplementedException("Some other control.property needs to be added for .Value Property Get")

            End Select

        End Get
        Set(myValue As T)

            Select Case True
                Case TypeOf _control Is TextBoxBase 
                    _control.Text = ConvertTo(Of String)(myValue)

                Case TypeOf _control Is ListControl 
                    'Its debatable should it be the list or the selected value
                    DataSource = myValue  
                    'Dim myListControl As ListControl = _control
                    'myListControl.DataSource = ConvertTo(Of T)(myValue)

                Case TypeOf _control Is RadioButton 
                    Dim myRadioButton As RadioButton = _control
                    myRadioButton.Checked = ConvertTo(Of Boolean)(myValue)

                Case Else
                    Throw New NotImplementedException("Some other control.property needs to be added for .Value Property Set")
            End Select

        End Set
    End Property

    Public Property Checked As Boolean Implements IWinformsControlAdapter(Of T).Checked
        Get
            If TypeOf _control IsNot RadioButton Then Throw New NotImplementedException()

            Dim myRadioButton As RadioButton = _control
            Return myRadioButton.Checked
        End Get
        Set(myValue As Boolean)
            If TypeOf _control IsNot RadioButton Then Throw New NotImplementedException()

            Dim myRadioButton As RadioButton = _control
            myRadioButton.Checked = myValue
        End Set
    End Property

    Public ReadOnly Property Completed As Boolean Implements IWinformsControlAdapter(Of T).Completed
        Get
            If TypeOf _control IsNot MaskedTextBox Then Throw New NotImplementedException()

            Dim myMaskedTextBox As MaskedTextBox = _control
            Return myMaskedTextBox.MaskCompleted
        End Get
    End Property

    Public Property DataSource As Object Implements IWinformsControlAdapter(Of T).DataSource
        Get
            If TypeOf _control IsNot ListControl Then Throw New NotImplementedException()

            Dim myListControl As ListControl = _control
            Return myListControl.DataSource
        End Get
        Set(myValue As Object)
            If TypeOf _control IsNot ListControl Then Throw New NotImplementedException()

            Dim myListControl As ListControl = _control
            myListControl.DataSource = myValue
        End Set
    End Property

    Public Property DisplayMember As String Implements IWinformsControlAdapter(Of T).DisplayMember
        Get
            If TypeOf _control IsNot ListControl Then Throw New NotImplementedException()

                Dim myListControl As ListControl = _control
                Return myListControl.DisplayMember
        End Get
        Set(myValue As String)
            If TypeOf _control IsNot ListControl Then Throw New NotImplementedException()

            Dim myListControl As ListControl = _control
            myListControl.DisplayMember = myValue
        End Set
    End Property
    Public Property ValueMember As String Implements IWinformsControlAdapter(Of T).ValueMember
        Get
            If TypeOf _control IsNot ListControl Then Throw New NotImplementedException()

            Dim myListControl As ListControl = _control
            Return myListControl.ValueMember
        End Get
        Set(myValue As String)
            If TypeOf _control IsNot ListControl Then Throw New NotImplementedException()

            Dim myListControl As ListControl = _control
            myListControl.ValueMember = myValue
        End Set
    End Property
    Public Property SelectedValue As Object Implements IWinformsControlAdapter(Of T).SelectedValue
        Get
            If TypeOf _control IsNot ListControl Then Throw New NotImplementedException()

            Dim myListControl As ListControl = _control
            Return myListControl.SelectedValue
        End Get
        Set(myValue As Object)
            If TypeOf _control IsNot ListControl Then Throw New NotImplementedException()

            Dim myListControl As ListControl = _control
            myListControl.SelectedValue = myValue
        End Set
    End Property
    Public Function ConvertTo(Of T)(myValue As Object) As T
        If (TypeOf myValue Is T) Then
            Dim variable As T = myValue
            Return variable
        End If

        Try
            'Handling Nullable types i.e, int?, double?, bool? .. etc
            If Nullable.GetUnderlyingType(GetType(T)) <> Nothing Then
                Return TypeDescriptor.GetConverter(GetType(T)).ConvertFrom(myValue)
            End If

            Return Convert.ChangeType(myValue, GetType(T))

        Catch ex As Exception
            Return CType(Nothing, T) 'Return the default value for the type - same as C#: return default(T)
        End Try
    End Function
    Public Property IsEnabled As Boolean Implements IWinformsControlAdapter(Of T).IsEnabled
        Get
            Return _control.Enabled
        End Get
        Set(myValue As Boolean)
            _control.Enabled = myValue
        End Set
    End Property    

    ''' <summary>
    ''' Sets the error description string for the UI Field.
    ''' </summary>
    ''' <param name="myValue">The error message to assign to the control</param>
    Friend Sub SetError(myValue As String) Implements IWinformsControlAdapter(Of T).SetError
        _errorProvider.SetError(_control, myValue)
    End Sub

    Sub [Select]() Implements IWinformsControlAdapter(Of T).Select
        If TypeOf _control Is TextBoxBase Then
            Dim myTextBoxBase As TextBoxBase = _control
            myTextBoxBase.Select(0, _control.Text.Length)
        End If
    End Sub

End Class

In the WinForm/View, declare Properties that will expose view controls as simple data types using the Adapter:

Property YesNo1 As IWinformsControlAdapter(Of Boolean) Implements IMyView.YesNo1
Property YesNo2 As IWinformsControlAdapter(Of Boolean) Implements IMyView.YesNo2 
Property StartDate As IWinformsControlAdapter(Of Date?) Implements IMyView.StartDate
Property EndDate As IWinformsControlAdapter(Of Date?) Implements IMyView.EndDate

In the WinForm/View constructor, wire up the properties to the Winforms UI controls (I am also using ErrorProvider):

Public Sub New() MyBase.New()

Try
    InitializeComponent()

    Yes = New WinformsControlAdapter(Of Boolean)(rdoHNSYes, ErrorProvider1)
    No = New WinformsControlAdapter(Of Boolean)(rdoHNSNo, ErrorProvider1)
    StartDate = New WinformsControlAdapter(Of Date?)(mskStartDate, ErrorProvider1)
    EndDate = New WinformsControlAdapter(Of Date?)(mskEndDate, ErrorProvider1)

In the Presenter: Private ReadOnly _view As IMyView

Public Sub New(view As IMyView)
    _view = view 'Inject dependency
End Sub

Now in your presenter code, you can use the adapter properties:

_view.YesNo1.Value = True 
If _view.StartDate.Completed Then
etc
etc

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM