So, this is the most fashionable code I've ever written. But it is useful that annoying. The reason for the whole repetition is that I want to maintain a free interface. If I extended the base class (in this case it will be View ), it returns only a View instance, which will prevent me from doing something like
let label = theme.CreateLabel().WithMargin(new Thickness(5.0)).WithText("Hello")
because the Label.Text property Label.Text not implemented by the base View class.
So here is my free interface. Get ready. It is ugly and repetitive. But it also works and is easy to use.
Did I miss the obvious way to dry it?
module ViewExtensions = let private withTwoWayBinding<'TElement, 'TProperty, 'TViewModel, 'TView when 'TView :> IViewFor<'TViewModel>>(viewModel: 'TViewModel, view: 'TView, viewModelProperty: Expr<'TViewModel -> 'TProperty>, viewProperty: Expr<'TView -> 'TProperty>) (element: 'TElement) = view.Bind(viewModel, ExpressionConversion.toLinq viewModelProperty, ExpressionConversion.toLinq viewProperty) |> ignore element let private withHorizontalOptions<'TElement when 'TElement :> View> options (element: 'TElement) = element.HorizontalOptions <- options element let private withVerticalOptions<'TElement when 'TElement :> View> options (element: 'TElement) = element.VerticalOptions <- options element let private withAlignment<'TElement when 'TElement :> View> horizontalOptions verticalOptions (control: 'TElement) = control |> withHorizontalOptions horizontalOptions |> withVerticalOptions verticalOptions let private withMargin<'TElement when 'TElement :> View> margin (element: 'TElement) = element.Margin <- margin element let private withActions<'TElement> (actions: ('TElement -> unit)[]) (element: 'TElement) = for action in actions do action(element) element type Xamarin.Forms.Entry with member this.WithHorizontalOptions(options) = withHorizontalOptions options this member this.WithVerticalOptions(options) = withHorizontalOptions options this member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this member this.WithTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) = withTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) this member this.WithMargin(margin) = withMargin margin this member this.With(actions) = withActions actions this member this.With(action: Entry -> unit) = this.With([|action|]) type Xamarin.Forms.Grid with member this.WithHorizontalOptions(options) = withHorizontalOptions options this member this.WithVerticalOptions(options) = withHorizontalOptions options this member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this member this.WithMargin(margin) = withMargin margin this member this.With(actions) = withActions actions this member this.With(action: Grid -> unit) = this.With([|action|]) type Xamarin.Forms.StackLayout with member this.WithHorizontalOptions(options) = withHorizontalOptions options this member this.WithVerticalOptions(options) = withHorizontalOptions options this member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this member this.WithMargin(margin) = withMargin margin this member this.With(actions) = withActions actions this member this.With(action: StackLayout -> unit) = this.With([|action|]) type Xamarin.Forms.Button with member this.WithHorizontalOptions(options) = withHorizontalOptions options this member this.WithVerticalOptions(options) = withHorizontalOptions options this member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this member this.WithMargin(margin) = withMargin margin this member this.WithText(text) = this.Text <- text; this member this.With(actions) = withActions actions this member this.With(action: Button -> unit) = this.With([|action|]) type Xamarin.Forms.Switch with member this.WithHorizontalOptions(options) = withHorizontalOptions options this member this.WithVerticalOptions(options) = withHorizontalOptions options this member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this member this.WithTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) = withTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) this member this.WithMargin(margin) = withMargin margin this member this.With(actions) = withActions actions this member this.With(action: Switch -> unit) = this.With([|action|]) type Xamarin.Forms.Label with member this.WithHorizontalOptions(options) = withHorizontalOptions options this member this.WithVerticalOptions(options) = withHorizontalOptions options this member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this member this.WithMargin(margin) = withMargin margin this member this.WithText(text) = this.Text <- text; this member this.With(actions) = withActions actions this member this.With(action: Label -> unit) = this.With([|action|])
UPDATE
So, thanks to your help, yes, I missed something obvious. As TheQuickBrownFox explained, if I changed the free interface to something like a form
let label = theme.CreateLabel() |> withMargin(new Thickness(5.0)) |> withContent("Hello")
then the monster that you see above can be completely replaced by
module ViewExtensions = let withTwoWayBinding<'TElement, 'TProperty, 'TViewModel, 'TView when 'TView :> IViewFor<'TViewModel>>(viewModel: 'TViewModel, view: 'TView, viewModelProperty: Expr<'TViewModel -> 'TProperty>, viewProperty: Expr<'TView -> 'TProperty>) (element: 'TElement) = view.Bind(viewModel, ExpressionConversion.toLinq viewModelProperty, ExpressionConversion.toLinq viewProperty) |> ignore element let withHorizontalOptions options (element: #View) = element.HorizontalOptions <- options; element let withVerticalOptions options (element: #View) = element.VerticalOptions <- options; element let withAlignment horizontalOptions verticalOptions element = element |> withHorizontalOptions horizontalOptions |> withVerticalOptions verticalOptions let withMargin margin (element: #View) = element.Margin <- margin; element let withCaption text (element: #Button) = element.Text <- text; element let withText text (element: #Entry) = element.Text <- text; element let withContent text (element: #Label) = element.Text <- text; element let withSetUpActions<'TElement> (actions: ('TElement -> unit)[]) (element: 'TElement) = (for action in actions do action(element)); element let withSetUpAction<'TElement> (action: 'TElement -> unit) = withSetUpActions([|action|])
This code deletion is very nice.