Home > front end >  Dynamic styling in .Net Maui/Xamarin
Dynamic styling in .Net Maui/Xamarin

Time:09-12

Let's say I want to display a calendar. Each day (DayViewModel) can have such attributes that should influence styling:

  • is day of current month
  • is weekend
  • is day available

And based on that I'd like to style my DayContentView like follows:

  • if day of current month -> opacity 1
  • if not current month -> opacity 0.5
  • if weekend -> background color is red
  • if available -> background color is green

Moreover the last one property ("is day available") can change based on user's action (clicked button "Check availability").

In other frameworks I'd create a few styles/style classes and assign them accordingly base on those flags. But since StyleClass is not bindable I cannot. Can I? Maybe there is some workaround.

Another option could be to create a separate style (using inheritance if needed) for all the combinations. But first of all the number of combinations raises like 2^n and the second issue is that I have no idea how to dynamically change whole styl for a view changing its name. Is it possible?

Finishing my quite long question: How to do it right/in a smart way? I don't want to store values of colors, font sizes, opacities etc. in a view model and bind all the values manually.

CodePudding user response:

I planned to add an example as @ToolmakerStave suggested but I found a solution in the meantime that seems to be very nice so I decided to share it instead. The idea is as follows:

  1. Declare view model that implements INotifyPropertyChanged
  2. Create a XAML file and define there all the styles
  3. In the code behind detect change of binding context (view model) and all its dynamically changing properties (IsAvailable) in my case
  4. When a change is detected then create a style using styles defined in the XAML file applying them in the desired order (making updates)
  5. Assign the final style to particular elements

So starting from my view model:

public class DayViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    // To style
    public bool IsCurrentMonth { get; set; }
    public bool IsWeekend { get; set; }
    public bool IsHoliday { get; set; }
    
    private bool _isAvailable;
    public bool IsAvailable
    {
        get => _isAvailable;
        set
        {
            _isAvailable = value;
            PropertyChanged?.Invoke(
                this,
                new PropertyChangedEventArgs(nameof(IsAvailable))
            );
        }
    }

    // To display
    public string DayOfWeek { get; set; }
    public int DayOfMonth { get; set; }
}

Assuming that only IsAvailable property can change after some user action.

Then I have a XAML view that defines all the needed styles but they are not used directly in this file. I'll use them in the code behind. All the elements that I need to style have x:Name property set to get a reference to them from the code:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:MyProject.Calendar.ViewModels"
             x:Class="MyProject.Calendar.ContentViews.DayView"
             x:DataType="vm:DayViewModel">

    <ContentView.Resources>
        <ResourceDictionary>
            <!-- By default Border has opacity 0.3 -->
            <Style TargetType="Border">
                <Setter Property="Opacity" Value="0.3" />
            </Style>

            <!-- These styles are used from the code behind -->
            <Style x:Key="CurrentMonthBorder" TargetType="Border">
                <Setter Property="Opacity" Value="1.0" />
            </Style>
            <Style x:Key="WeekendBorder" TargetType="Border">
                <Setter Property="BackgroundColor" Value="LightGray" />
            </Style>
            <Style x:Key="AvailableBorder" TargetType="Border">
                <Setter Property="BackgroundColor" Value="Green" />
            </Style>

            <Style x:Key="HolidayLabel" TargetType="Label">
                <Setter Property="TextColor" Value="DarkRed" />
            </Style>
            <!-- // These styles are used from the code behind -->
        </ResourceDictionary>
    </ContentView.Resources>

    <Border x:Name="DayBorder">
        <VerticalStackLayout HorizontalOptions="FillAndExpand">
            <Label x:Name="DayOfWeekLabel" Text="{Binding DayOfWeek}" />
            <Label x:Name="DayOfMonthLabel" Text="{Binding DayOfMonth}"  />
        </VerticalStackLayout>
    </Border>
</ContentView>

Now a helper class that allows to update a style with another style:

public static class StyleExtension
{
    public static Style Update(this Style style, Style otherStyle)
    {
        var result = new Style(style.TargetType);
        var allSetters = style.Setters.Concat(otherStyle.Setters);
        foreach (var setter in allSetters)
        {
            result.Setters.Add(setter);
        }

        return result;
    }

    public static Style UpdateIf(this Style style, bool condition, Style otherStyle)
    {
        return style.Update(condition ? otherStyle : new Style(style.TargetType));
    }
}

Final part, the code behind:

public partial class DayView : ContentView
{
    private DayViewModel _viewModel;

    public DayView()
    {
        InitializeComponent();
        BindingContextChanged  = OnBindingContextChanged;
    }


    private void OnBindingContextChanged(object sender, EventArgs e)
    {
        _viewModel = BindingContext as DayViewModel;
        _viewModel.PropertyChanged  = OnAvailabilityChanged;   

        StyleDayBorder();
        StyleDayOfWeekLabel();
        StyleDayOfMonthLabel();
    }

    private void OnAvailabilityChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(DayViewModel.IsAvailable))
        {
            StyleDayBorder();
        }
    }

    private void StyleDayBorder()
    {
        DayBorder.Style = new Style(typeof(Border))
            .UpdateIf(_viewModel.IsCurrentMonth, GetStyle("CurrentMonthBorder"))
            .UpdateIf(_viewModel.IsWeekend, GetStyle("WeekendBorder"))
            .UpdateIf(_viewModel.IsAvailable, GetStyle("AvailableBorder"));
    }

    private void StyleDayOfWeekLabel()
    {
        DayOfWeekLabel.Style = new Style(typeof(Label))
            .UpdateIf(_viewModel.IsHoliday, GetStyle("HolidayLabel"));
    }

    private void StyleDayOfMonthLabel()
    {
        DayOfMonthLabel.Style = new Style(typeof(Label))
            .UpdateIf(_viewModel.IsHoliday, GetStyle("HolidayLabel"));
    }

    private Style GetStyle(string name)
    {
        return Resources[name] as Style;
    }
}

And final thought after the final code:) If there is no simpler solution then it would be nice to be able to do it in the XAML like this (pseudocode):

<Label Text="{Binding Message}">
    <Label.Styles>
        <Style ShouldApply="{Binding IsError}" Style="{StaticResource ErrorMessage}" />
        <Style ShouldApply="{Binding IsWarning}" Style="{StaticResource WarningMessage}" />
        ...
    </Label.Styles>
</Label>

And then all the styles would be created applying one after another in the defined order based on the defined conditions.

CodePudding user response:

The same thing can be achieved using DataTriggers.

Result:

result

MainPage.xaml

<Window
    x:Class="WpfApp6.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfApp6"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">

    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>

    <ItemsControl Margin="5" ItemsSource="{Binding Days}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="{x:Type local:DayModel}">
                <Border
                    Padding="10"
                    Width="200"
                    Height="100"
                    Margin="5">
                    <Border.Style>
                        <Style TargetType="Border">
                            <Setter Property="Background" Value="Gray" />
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding IsDayOfCurrentMonth}" Value="False">
                                    <Setter Property="Opacity" Value="0.5" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding IsAvailable}" Value="True">
                                    <Setter Property="Background" Value="Green" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding IsWeekend}" Value="True">
                                    <Setter Property="Background" Value="Red" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Border.Style>
                    <StackPanel>
                        <TextBlock Text="{Binding IsDayOfCurrentMonth, StringFormat='IsDayOfCurrentMonth: {0}'}" />
                        <TextBlock Text="{Binding IsAvailable, StringFormat='IsAvailable: {0}'}" />
                        <TextBlock Text="{Binding IsWeekend, StringFormat='IsWeekend: {0}'}" />
                    </StackPanel>
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Window>

DayModel.cs

public record DayModel(bool IsDayOfCurrentMonth, bool IsWeekend, bool IsAvailable);

MainViewModel.cs

public class MainViewModel
{
    public ObservableCollection<DayModel> Days { get; } = new();

    public MainViewModel()
    {
        //Create all possible cominations of days
        for (int i = 0; i < 2; i  )
        {
            for (int j = 0; j < 2; j  )
            {
                for (int k = 0; k < 2; k  )
                {
                    Days.Add(new DayModel(i == 0, j == 0, k == 0));
                }
            }
        }
    }
}
  • Related