question

AdrianJakubcik-3866 avatar image
0 Votes"
AdrianJakubcik-3866 asked JarvanZhang-MSFT commented

How to Define and Initialize Custom Control Layout in Code

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
 };








dotnet-csharpdotnet-xamarinforms
· 4
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.


What issues or errors do you have when you create the test_obj object?

0 Votes 0 ·

No issues nor errors occur, however initializing the object the way I've shown gives a blank content view. Since all the properties of the object have their default values (mostly blank) and not the ones I've set when initializing the object. This is due to me laying out the control in the constructor. However, I don't know where else to lay out the ContentView with the properties that are set during object initialization.

0 Votes 0 ·

To be more specific, when I initialize the ImageLabel object with the properties being set during initialization.

 var test_obj = new ImageLabel
 {
     ImageAlignment = Alignment.Up,
     ImageCornerRadius = 15f,
     ImagePadding = new Thickness(10),
     Text = "Hello World",
     TextPadding = new Thickness(8),
     TextAttributes = FontAttributes.Bold
 };

and let's say I print this into some ContentPage I get a blank object, because of the default values the individual controls have if not set...

 public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(CustomUI), string.Empty);
    
 public string Text
 {
     get => (string)GetValue(TextProperty);
     set => SetValue(TextProperty, value);
 }

As you can see the default value is string.Empty, and that's exactly what the label text is assigned in the constructor during initialization

 public ImageLabel()
 {
             _layout = new FlexLayout { Direction = _getFlexDir(ImageAlignment), JustifyContent = FlexJustify.Center, AlignItems = FlexAlignItems.Center, AlignContent = FlexAlignContent.Center, Padding = this.Padding, BackgroundColor = Color.Blue};
    
             var _label = new Label { FontFamily = this.FontFamily, FontSize = this.FontSize, Text = this.Text, TextColor = this.TextColor, Padding = this.TextPadding, FontAttributes = this.TextAttributes, };
 }


0 Votes 0 ·
Viorel-1 avatar image Viorel-1 AdrianJakubcik-3866 ·

It seems that you should use "binding". For example, when you create the Image, you must define a binding for its Source property. WPF has the SetBinding function; maybe there is a similar feature in Xamarin. In this case, assigning a value to ImageLabel.Source (during construction or later) will be observed by the Image control. Currently the Image control is not informed about the assignments to ImageSourceProperty.

Maybe a simple workaround is also this:

 public ImageSource Source
 {
    set
    {
       SetValue(ImageSourceProperty, value);
    
       // TODO: also assign the new source to child Image control.
       // Something like this:
    
       _frame = ... 
       ((Image)_frame.Content).Source = value;
    }
    
 }

0 Votes 0 ·
JarvanZhang-MSFT avatar image
1 Vote"
JarvanZhang-MSFT answered AdrianJakubcik-3866 converted comment to answer

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.


5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

AdrianJakubcik-3866 avatar image
0 Votes"
AdrianJakubcik-3866 answered JarvanZhang-MSFT commented

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...

· 4
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

@JarvanZhang-MSFT

I even tried adding it directly to XAML like so...

 <Grid BindingContext="{x:Reference this}" x:Name="Header" BackgroundColor="{Binding Background}" VerticalOptions="Start" HorizontalOptions="Fill" Padding="{Binding InnerPadding}" MinimumHeightRequest="{Binding Height}">
         <StackLayout x:Name="leftContainer" Orientation="Horizontal" HeightRequest="{Binding IconHeight}">
             <ImageButton Source="https://cdn-icons-png.flaticon.com/512/271/271218.png" Padding="6,6" HeightRequest="{Binding IconHeight}" CornerRadius="50" BackgroundColor="Transparent"  Aspect="AspectFit" IsVisible="{Binding hasBackButton}" IsEnabled="{Binding hasBackButton}" Clicked="BackButton_Clicked"/>
         </StackLayout>
         <StackLayout x:Name="IconLayout" Orientation="Horizontal" HorizontalOptions="Center" VerticalOptions="Center" HeightRequest="{Binding IconHeight}">
             <Image Source="{Binding Icon}" VerticalOptions="CenterAndExpand" HorizontalOptions="Center" Aspect="AspectFit"/>
             <Label Text="{Binding Title}" VerticalOptions="CenterAndExpand" HorizontalOptions="Center" FontSize="Large" Padding="0" TextColor="{Binding TitleColor}"/>
         </StackLayout>
         <StackLayout x:Name="rightContainer" Orientation="Horizontal" HeightRequest="{Binding IconHeight}">
                
         </StackLayout>
     </Grid>

But no success... clicking the button doesn't fire the event that should call my method BackButton_Clicked(object sender, EventArgs e)

Sorry for not formatting the XAML code

0 Votes 0 ·
JarvanZhang-MSFT avatar image JarvanZhang-MSFT AdrianJakubcik-3866 ·

We need to define the 'BackButtonPressed' event in the contentPage. I tested a basic demo, it works as expected. Here is the sample code, you could refer to it.

Consume the custom control.

<ContentPage ...
    x:Class="TestApplication_6.TestPage">
    <ContentPage.Content>
        <StackLayout>
            <local:CustomHeader BackButtonPressed="CustomHeader_BackButtonPressed"/>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

//page.xaml.cs
public partial class TestPage : ContentPage
{
    ...
    private void CustomHeader_BackButtonPressed(object sender, EventArgs e)
    {
        DisplayAlert("title", "message", "cancel");
    }
}

Custom control

<ContentView ...
    x:Class="TestApplication_6.CustomView">
    <ContentView.Content>
        <StackLayout x:Name="layout">
        </StackLayout>
    </ContentView.Content>
</ContentView>

public partial class CustomView : ContentView
{
    public event EventHandler BackButtonPressed;
    public CustomView()
    {
        InitializeComponent();

        ImageButton button = new ImageButton
        {
            Source = "image_source",
            Padding = new Thickness(5, 5),
            CornerRadius = 50,
        };
        button.Clicked += Button_Clicked;
        layout.Children.Add(button);
    }

    private void Button_Clicked(object sender, EventArgs e)
    {
        BackButtonPressed.Invoke(sender, e);
    }
}

1 Vote 1 ·

Thank you very much I found the bug, I defined the button inside a stacklayout that was inside a grid... however I didn't specify the Grid.Row and Grid.Column for the stacklayout and therefore the program didn't notify the click on a non-existing element. However, visually everything was as expected so I didn't notice such an error.

1 Vote 1 ·
Show more comments