Skip to content

Commit

Permalink
Introducing SwitchConverter! 🦙❤️
Browse files Browse the repository at this point in the history
Migrate core logic from SwitchPresenter and generlize to share between both SwitchPresenter and SwitchConverter
Added small demo of SwitchConverter to SwitchPresenter docs
Tested on UWP, WinUI 3, and Uno Platform
  • Loading branch information
michael-hawker committed Nov 1, 2024
1 parent 7349a30 commit 4ebc5a3
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 119 deletions.
6 changes: 6 additions & 0 deletions components/Primitives/samples/SwitchPresenter.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ We can also invert the paradigm a bit with a `SwitchPresenter` to do data transf
> [!SAMPLE SwitchPresenterTemplateSample]
That's right! `SwitchPresenter` can be used not just for displaying different UIElements but in feeding different kinds of data into the `ContentTemplate` as well.

## SwitchConverter

A new analog to `SwitchPresenter` is the `SwitchConverter` which can be used in bindings to translate values into resources:

> [!SAMPLE SwitchConverterBrushSample]
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<Page x:Class="PrimitivesExperiment.Samples.SwitchPresenter.SwitchConverterBrushSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:PrimitivesExperiment.Samples.SwitchPresenter"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">

<Page.Resources>
<!--
If you reference an enum directly in UWP, you need to use it somewhere for the XamlTypeInfo reference to be generated...
-->
<local:CheckStatus x:Key="MyChecks">Warning</local:CheckStatus>

<!-- SwitchConverter lets you easily convert general values to resources -->
<!-- Note: This is in the converters namespace -->
<converters:SwitchConverter x:Key="StatusToColorSwitchConverter"
TargetType="local:CheckStatus">
<!-- Note: These are reused from the controls namespace from SwitchPresenter -->
<controls:Case Content="{ThemeResource SystemFillColorCriticalBrush}"
Value="Error" />
<controls:Case Content="{ThemeResource SystemFillColorCautionBrush}"
Value="Warning" />
<controls:Case Content="{ThemeResource SystemFillColorSuccessBrush}"
Value="Success" />
</converters:SwitchConverter>
</Page.Resources>

<StackPanel Spacing="8">
<ComboBox x:Name="StatusPicker"
Header="Pick a status"
SelectedIndex="0">
<x:String>Error</x:String>
<x:String>Warning</x:String>
<x:String>Success</x:String>
</ComboBox>
<TextBlock Text="This is it, this is the demo:" />
<TextBlock FontWeight="SemiBold"
Foreground="{x:Bind StatusPicker.SelectedItem, Converter={StaticResource StatusToColorSwitchConverter}, Mode=OneWay}"
Text="{x:Bind StatusPicker.SelectedItem, Mode=OneWay}" />
</StackPanel>
</Page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.WinUI.Converters;

namespace PrimitivesExperiment.Samples.SwitchPresenter;

[ToolkitSample(id: nameof(SwitchConverterBrushSample), "SwitchConverter Brush", description: $"A sample for showing how to use a {nameof(SwitchConverter)} for swapping a brush based on an enum.")]
public sealed partial class SwitchConverterBrushSample : Page
{
public SwitchConverterBrushSample()
{
this.InitializeComponent();
}
}

public enum CheckStatus
{
Error,
Warning,
Success,
}
80 changes: 80 additions & 0 deletions components/Primitives/src/SwitchPresenter/SwitchConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.WinUI.Controls;

namespace CommunityToolkit.WinUI.Converters;

/// <summary>
/// A helper <see cref="IValueConverter"/> which can automatically translate incoming data to a set of resulting values defined in XAML.
/// </summary>
/// <example>
/// &lt;converters:SwitchConverter x:Key=&quot;StatusToColorSwitchConverter&quot;
/// TargetType=&quot;models:CheckStatus&quot;&gt;
/// &lt;controls:Case Value=&quot;Error&quot; Content=&quot;{ThemeResource SystemFillColorCriticalBrush}&quot;/&gt;
/// &lt;controls:Case Value=&quot;Warning&quot; Content=&quot;{ThemeResource SystemFillColorCautionBrush}&quot;/&gt;
/// &lt;controls:Case Value=&quot;Success&quot; Content=&quot;{ThemeResource SystemFillColorSuccessBrush}&quot;/&gt;
/// &lt;/converters:SwitchConverter&gt;
/// ...
/// &lt;TextBlock
/// FontWeight=&quot;SemiBold&quot;
/// Foreground=&quot;{x:Bind Status, Converter={StaticResource StatusToColorSwitchConverter}}&quot;
/// Text = &quot;{x:Bind Status}&quot; /&gt;
/// </example>
[ContentProperty(Name = nameof(SwitchCases))]
public sealed partial class SwitchConverter : DependencyObject, IValueConverter
{
/// <summary>
/// Gets or sets a value representing the collection of cases to evaluate.
/// </summary>
public CaseCollection SwitchCases
{
get { return (CaseCollection)GetValue(SwitchCasesProperty); }
set { SetValue(SwitchCasesProperty, value); }
}

/// <summary>
/// Identifies the <see cref="SwitchCases"/> property.
/// </summary>
public static readonly DependencyProperty SwitchCasesProperty =
DependencyProperty.Register(nameof(SwitchCases), typeof(CaseCollection), typeof(SwitchConverter), new PropertyMetadata(null));

/// <summary>
/// Gets or sets a value indicating which type to first cast and compare provided values against.
/// </summary>
public Type TargetType
{
get { return (Type)GetValue(TargetTypeProperty); }
set { SetValue(TargetTypeProperty, value); }
}

/// <summary>
/// Identifies the <see cref="TargetType"/> property.
/// </summary>
public static readonly DependencyProperty TargetTypeProperty =
DependencyProperty.Register(nameof(TargetType), typeof(Type), typeof(SwitchConverter), new PropertyMetadata(null));

/// <summary>
/// Initializes a new instance of the <see cref="SwitchPresenter"/> class.
/// </summary>
public SwitchConverter()
{
// Note: we need to initialize this here so that XAML can automatically add cases without needing this defined around it as the content.
// We don't do this in the PropertyMetadata as then we create a static shared collection for all converters, which we don't want. We want it per instance.
// See https://learn.microsoft.com/windows/uwp/xaml-platform/custom-dependency-properties#initializing-the-collection
SwitchCases = new CaseCollection();
}

public object Convert(object value, Type targetType, object parameter, string language)
{
var result = SwitchCases.EvaluateCases(value, TargetType ?? targetType);

return result?.Content!;
}

public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
133 changes: 133 additions & 0 deletions components/Primitives/src/SwitchPresenter/SwitchHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace CommunityToolkit.WinUI.Controls;

/// <summary>
/// Internal helpers for use between <see cref="SwitchPresenter"/> and <see cref="SwitchConverter"/>.
/// The logic here is the main code which looks across a <see cref="CaseCollection"/> to match a specific <see cref="Case"/> with a given value while converting types based on the <see cref="SwitchPresenter.TargetType"/> property. This will handle <see cref="Enum"/> values as well as values compatible with the <see cref="XamlBindingHelper.ConvertValue(Type, object)"/> method.
/// </summary>
internal static partial class SwitchHelpers
{
/// <summary>
/// Extension method for a set of cases to find the matching case given its value and type.
/// </summary>
/// <param name="switchCases">The collection of <see cref="Case"/>s in a <see cref="CaseCollection"/></param>
/// <param name="value">The value of the <see cref="Case"/> to find</param>
/// <param name="targetType">The desired type of the result for automatic conversion</param>
/// <returns>The discovered value, the default value, or <c>null</c></returns>
internal static Case? EvaluateCases(this CaseCollection switchCases, object value, Type targetType)
{
if (switchCases == null ||
switchCases.Count == 0)
{
// If we have no cases, then we can't match anything.
return null;
}

Case? xdefault = null;
Case? newcase = null;

foreach (Case xcase in switchCases)
{
if (xcase.IsDefault)
{
// If there are multiple default cases provided, this will override and just grab the last one, the developer will have to fix this in their XAML. We call this out in the case comments.
xdefault = xcase;
continue;
}

if (CompareValues(value, xcase.Value, targetType))
{
newcase = xcase;
break;
}
}

if (newcase == null && xdefault != null)
{
// Inject default if we found one without matching anything
newcase = xdefault;
}

return newcase;
}

/// <summary>
/// Compares two values using the TargetType.
/// </summary>
/// <param name="compare">Our main value in our SwitchPresenter.</param>
/// <param name="value">The value from the case to compare to.</param>
/// <returns>true if the two values are equal</returns>
internal static bool CompareValues(object compare, object value, Type targetType)
{
if (compare == null || value == null)
{
return compare == value;
}

if (targetType == null ||
(targetType == compare.GetType() &&
targetType == value.GetType()))
{
// Default direct object comparison or we're all the proper type
return compare.Equals(value);
}
else if (compare.GetType() == targetType)
{
// If we have a TargetType and the first value is the right type
// Then our 2nd value isn't, so convert to string and coerce.
var valueBase2 = ConvertValue(targetType, value);

return compare.Equals(valueBase2);
}

// Neither of our two values matches the type so
// we'll convert both to a String and try and coerce it to the proper type.
var compareBase = ConvertValue(targetType, compare);

var valueBase = ConvertValue(targetType, value);

return compareBase.Equals(valueBase);
}

/// <summary>
/// Helper method to convert a value from a source type to a target type.
/// </summary>
/// <param name="targetType">The target type</param>
/// <param name="value">The value to convert</param>
/// <returns>The converted value</returns>
internal static object ConvertValue(Type targetType, object value)
{
if (targetType.IsInstanceOfType(value))
{
return value;
}
else if (targetType.IsEnum && value is string str)
{
#if HAS_UNO
if (Enum.IsDefined(targetType, str))
{
return Enum.Parse(targetType, str);
}
#else
if (Enum.TryParse(targetType, str, out object? result))
{
return result!;
}
#endif

static object ThrowExceptionForKeyNotFound()
{
throw new InvalidOperationException("The requested enum value was not present in the provided type.");
}

return ThrowExceptionForKeyNotFound();
}
else
{
return XamlBindingHelper.ConvertValue(targetType, value);
}
}
}
Loading

0 comments on commit 4ebc5a3

Please sign in to comment.