How to Define and Initialize Custom Control Layout in Code

Adrian Jakubcik 111 Reputation points
2021-09-27T10:14:20.473+00:00

Lately, I was trying to create Custom Control using some documentation. On the Microsoft page, a note in the documentation for creating a custom control says

It's possible to create a custom control whose layout is defined in code instead of XAML.

And so I've created something like this:

public abstract partial class CustomUI : ContentView
    {
        public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(CustomUI), string.Empty);
        public static readonly BindableProperty ColorProperty = BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(CustomUI), Color.Black);

        public string Text
        {
            get => (string)GetValue(TextProperty);
            set => SetValue(TextProperty, value);
        }

        public Color TextColor
        {
            get => (Color)GetValue(ColorProperty);
            set => SetValue(ColorProperty, value);
        }

        internal FlexDirection _getFlexDir(Alignment Alignment)
        {
            return (Alignment == Alignment.Up || Alignment == Alignment.Down) ? FlexDirection.Column : FlexDirection.Row;
        }
        internal StackOrientation _getStackOrient(Alignment Alignment)
        {
            return (Alignment == Alignment.Up || Alignment == Alignment.Down) ? StackOrientation.Vertical : StackOrientation.Horizontal;
        }
    }

public class ImageLabel : CustomUI
    {
        public FlexLayout _layout;
        public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(ImageLabel), default(string));
        public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(nameof(FontSize), typeof(double), typeof(ImageLabel), (double)10);
        public static readonly BindableProperty FontAttributesProperty = BindableProperty.Create(nameof(TextAttributes), typeof(FontAttributes), typeof(ImageLabel), default(FontAttributes));
        public static readonly BindableProperty TextPaddingProperty = BindableProperty.Create(nameof(TextPadding), typeof(Thickness), typeof(ImageLabel), new Thickness(0));
        public static readonly BindableProperty ImageAlignmentProperty = BindableProperty.Create(nameof(ImageAlignment), typeof(Alignment), typeof(ImageLabel), default(Alignment));
        public static readonly BindableProperty ImageCornerRadiusProperty = BindableProperty.Create(nameof(ImageCornerRadius), typeof(float), typeof(ImageLabel), 0f);
        public static readonly BindableProperty ImagePaddingProperty = BindableProperty.Create(nameof(ImagePadding), typeof(Thickness), typeof(ImageLabel), new Thickness(0));
        public static readonly BindableProperty ImageBackgroundColorProperty = BindableProperty.Create(nameof(ImageBackgroundColor), typeof(Color), typeof(ImageLabel), Color.Transparent);
        public static readonly BindableProperty ImageSourceProperty = BindableProperty.Create(nameof(Source), typeof(ImageSource), typeof(ImageLabel), default(ImageSource));
        public static readonly BindableProperty ImageHeightProperty = BindableProperty.Create(nameof(ImgHeight), typeof(double), typeof(ImageLabel), null);
        public static readonly BindableProperty ImageWidthProperty = BindableProperty.Create(nameof(ImgWidth), typeof(double), typeof(ImageLabel), null);
        public static readonly BindableProperty ImageMarginProperty = BindableProperty.Create(nameof(Margin), typeof(Thickness), typeof(ImageLabel), new Thickness(0));

        public ImageLabel()
        {
            _layout = new FlexLayout { Direction = CustomUI._getFlexDir(ImageAlignment), JustifyContent = FlexJustify.Center, AlignItems = FlexAlignItems.Center, AlignContent = FlexAlignContent.Center, Padding = this.Padding, BackgroundColor = Color.Blue};
            var _frame = new Frame { CornerRadius = ImageCornerRadius, Padding = ImagePadding, BackgroundColor = ImageBackgroundColor };
            _frame.Content = new Image { Margin = Margin, HeightRequest = ImgHeight, Source = Source };
            var _label = new Label { FontFamily = this.FontFamily, FontSize = this.FontSize, Text = this.Text, TextColor = this.TextColor, Padding = this.TextPadding, FontAttributes = this.TextAttributes, };

            if (ImageAlignment == Alignment.Up || ImageAlignment == Alignment.Left)
            {
                _layout.Children.Add(_frame);
                _layout.Children.Add(_label);
            } 
            else
            {
                _layout.Children.Add(_label);
                _layout.Children.Add(_frame);
            }

            this.Content = _layout;
        }

        public FontAttributes TextAttributes
        {
            get => (FontAttributes)GetValue(FontAttributesProperty);
            set => SetValue(FontAttributesProperty, value);
        }

        public Thickness TextPadding
        {
            get => (Thickness)GetValue(TextPaddingProperty);
            set => SetValue(TextPaddingProperty, value);
        }

        public string FontFamily
        {
            get => (string)GetValue(FontFamilyProperty);
            set => SetValue(FontFamilyProperty, value);
        }

        public double FontSize
        {
            get => (double)GetValue(FontSizeProperty);
            set => SetValue(FontSizeProperty, value);
        }

        public Color ImageBackgroundColor
        {
            get => (Color)base.GetValue(ImageBackgroundColorProperty);
            set => SetValue(ImageBackgroundColorProperty, value);
        }

        public Thickness ImagePadding
        {
            get => (Thickness)GetValue(ImagePaddingProperty);
            set => SetValue(ImagePaddingProperty, value);
        }

        public float ImageCornerRadius
        {
            get => (float)GetValue(ImageCornerRadiusProperty);
            set => SetValue(ImageCornerRadiusProperty, value);
        }

        public ImageSource Source
        {
            get => (ImageSource)GetValue(ImageSourceProperty);
            set => SetValue(ImageSourceProperty, value);
        }

        public double ImgHeight
        {
            get => (double)GetValue(ImageHeightProperty);
            set => SetValue(ImageHeightProperty, value);
        }

        public double ImgWidth
        {
            get => (double)GetValue(ImageWidthProperty);
            set => SetValue(ImageWidthProperty, value);
        }

        public Alignment ImageAlignment
        {
            get => (Alignment)GetValue(ImageAlignmentProperty);
            set => SetValue(ImageAlignmentProperty, value);
        }
    }

    public enum Alignment
    {
       Left, Up, Right, Down
    }

However, defining the layout in the constructor is a really bad idea, since all the bindable variables are not set yet. So, my question is how to properly define the layout in code rather than in XAML... and how to initialize it later on because with the layout defined in the constructor I can't simply do what I do with all the other UI elements...

var test_obj = new ImageLabel
{
    ImageAlignment = Alignment.Right,
    ImgHeight = 48,
    Margin = new Thickness(4),
    Source = ImageSource.FromUri(new Uri("https://upload.wikimedia.org/wikipedia/commons/thumb/6/69/How_to_use_icon.svg/1200px-How_to_use_icon.svg.png")),
    ImageBackgroundColor = Color.Red,
    ImageCornerRadius = 15f,
    ImagePadding = new Thickness(10),
    Text = "Hello World",
    FontSize = 20,
    TextPadding = new Thickness(8),
    TextAttributes = FontAttributes.Bold
};
Xamarin
Xamarin
A Microsoft open-source app platform for building Android and iOS apps with .NET and C#.
5,367 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
11,097 questions
{count} votes

Accepted answer
  1. JarvanZhang 23,961 Reputation points
    2021-09-28T03:09:46.507+00:00

    Hello,​

    Welcome to our Microsoft Q&A platform!

    The view is rendered after it's initialized. So the values of the parameters will be null(or default values) if you set data to the views in the constructor method of the custom control. To avoid this, try using data binding instead. Set the custom control as the BindingContext, the binding paths are the name of the parameters. The layout's direction is defined accroding to the value of the alignment, you could detect the property changed event to check the value.

    Here is the sample code, you could refer to it.

       public class ImageLabel : CustomUI  
       {  
           //change the _layout to be a static property  
           public static FlexLayout _layout;  
         
           //add property changed event to ImageAlignmentProperty to set value to '_layout.Direction' according to the alignment value  
           public static readonly BindableProperty ImageAlignmentProperty = BindableProperty.Create(nameof(ImageAlignment), typeof(Alignment), typeof(ImageLabel), propertyChanged: OnImageAlignmentChanged);  
         
           //property changed event  
           private static void OnImageAlignmentChanged(BindableObject bindable, object oldValue, object newValue)  
           {  
               var alignment = (Alignment)newValue;  
               _layout.Direction = (alignment == Alignment.Up || alignment == Alignment.Down) ? FlexDirection.Column : FlexDirection.Row;  
           }  
         
           public ImageLabel()  
           {  
               _layout = new FlexLayout  
               {  
                   //Direction = this._getFlexDir(ImageAlignment),   
                   JustifyContent = FlexJustify.Center,  
                   AlignItems = FlexAlignItems.Center,  
                   AlignContent = FlexAlignContent.Center,  
                   Padding = this.Padding,  
                   BackgroundColor = Color.Blue  
               };  
               _layout.SetBinding(FlexLayout.PaddingProperty, "Padding");  
         
               var _frame = new Frame  
               {  
                   //CornerRadius = ImageCornerRadius,  
                   //Padding = ImagePadding,  
                   //BackgroundColor = ImageBackgroundColor  
               };  
               _frame.SetBinding(Frame.CornerRadiusProperty, "ImageCornerRadius");  
               _frame.SetBinding(Frame.PaddingProperty, "ImagePadding");  
               _frame.SetBinding(Frame.BackgroundColorProperty, "ImageBackgroundColor");  
         
               Image img = new Image  
               {  
                   //Margin = Margin,   
                   //HeightRequest = ImgHeight,   
                   //Source = Source   
               };  
               img.SetBinding(Image.MarginProperty, "Margin");  
               img.SetBinding(Image.HeightProperty, "ImgHeight");  
               img.SetBinding(Image.SourceProperty, "Source");  
               _frame.Content = img;  
         
               var _label = new Label  
               {  
                   //FontFamily = this.FontFamily,  
                   //FontSize = this.FontSize,  
                   //Text = this.Text,  
                   //TextColor = this.TextColor,  
                   //Padding = this.TextPadding,  
                   //FontAttributes = this.TextAttributes,  
               };  
               _label.SetBinding(Label.TextProperty, "Text");  
               _label.SetBinding(Label.FontFamilyProperty, "FontFamily");  
               _label.SetBinding(Label.TextColorProperty, "TextColor");  
               _label.SetBinding(Label.PaddingProperty, "TextPadding");  
               _label.SetBinding(Label.FontAttributesProperty, "TextAttributes");  
         
               if (ImageAlignment == Alignment.Up || ImageAlignment == Alignment.Left)  
               {  
                   _layout.Children.Add(_frame);  
                   _layout.Children.Add(_label);  
               }  
               else  
               {  
                   _layout.Children.Add(_label);  
                   _layout.Children.Add(_frame);  
               }  
         
               this.Content = _layout;  
               BindingContext = this;  
           }  
       }  
    

    Best Regards,

    Jarvan Zhang


    If the response is helpful, please click "Accept Answer" and upvote it.

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    1 person found this answer helpful.
    0 comments No comments

1 additional answer

Sort by: Most helpful
  1. Adrian Jakubcik 111 Reputation points
    2021-09-28T19:05:07.39+00:00

    Thank you very much for clarifying how it's done, but I also have another question regarding this. When I have a button inside my custom control how can I bind its event? Because I created a custom navigation bar and I have a back button there. However, inside the custom control, I can't set a default method for the button clicked to go back in navigation like so

    private async void BackButton_Clicked(object sender, EventArgs e)
    {
        await Navigation.PopModalAsync();
    }
    

    So I created an EventHandler which is passed and on every individual page I will have to set the function again and again... well it doesn't work that way too

    public partial class CustomHeader : ContentView
        {
            public event EventHandler BackButtonPressed;
    
            private void BackButton_Clicked(object sender, EventArgs e)
            {
                Console.WriteLine($"{nameof(BackButton_Clicked)} has been called!");
                BackButtonPressed?.Invoke(sender, e);
            }
    
            public CustomHeader()
            {
                InitializeComponent();
    
                var backButton = new ImageButton
                {
                    Source = ImageSource.FromUri(new Uri("https://www.example.com")),
                    Padding = new Thickness(5,5),
                    CornerRadius = 50,
                };
                backButton.Clicked += BackButton_Clicked;
                leftContainer.Children.Add(backButton);
            }
        }
    

    The BackButton_Clicked() is never called...


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.