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:
- Declare view model that implements
INotifyPropertyChanged
- Create a XAML file and define there all the styles
- In the code behind detect change of binding context (view model) and all its dynamically changing properties (
IsAvailable
) in my case - When a change is detected then create a style using styles defined in the XAML file applying them in the desired order (making updates)
- 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 DataTrigger
s.
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));
}
}
}
}
}