diff --git a/MainDemo.Wpf/RatingBar.xaml b/MainDemo.Wpf/RatingBar.xaml
index b546736aac..7bf633758c 100644
--- a/MainDemo.Wpf/RatingBar.xaml
+++ b/MainDemo.Wpf/RatingBar.xaml
@@ -79,6 +79,180 @@
VerticalAlignment="Top"
Margin="10,2,0,0"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MainDemo.Wpf/RatingBar.xaml.cs b/MainDemo.Wpf/RatingBar.xaml.cs
index 3e5a5ec5a8..0b0c96ad7f 100644
--- a/MainDemo.Wpf/RatingBar.xaml.cs
+++ b/MainDemo.Wpf/RatingBar.xaml.cs
@@ -7,7 +7,10 @@ public partial class RatingBar
{
public RatingBar() => InitializeComponent();
- private void BasicRatingBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ private void BasicRatingBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
=> Debug.WriteLine($"BasicRatingBar value changed from {e.OldValue} to {e.NewValue}.");
+
+ private void BasicRatingBarFractional_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ => Debug.WriteLine($"BasicRatingBarFractional value changed from {e.OldValue} to {e.NewValue}.");
}
}
diff --git a/MaterialDesign3.Demo.Wpf/RatingBar.xaml.cs b/MaterialDesign3.Demo.Wpf/RatingBar.xaml.cs
index f65addb1db..7100128eac 100644
--- a/MaterialDesign3.Demo.Wpf/RatingBar.xaml.cs
+++ b/MaterialDesign3.Demo.Wpf/RatingBar.xaml.cs
@@ -7,7 +7,7 @@ public partial class RatingBar
{
public RatingBar() => InitializeComponent();
- private void BasicRatingBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ private void BasicRatingBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
=> Debug.WriteLine($"BasicRatingBar value changed from {e.OldValue} to {e.NewValue}.");
}
}
diff --git a/MaterialDesignThemes.Wpf.Tests/RatingBarTests.cs b/MaterialDesignThemes.Wpf.Tests/RatingBarTests.cs
new file mode 100644
index 0000000000..352dafba2c
--- /dev/null
+++ b/MaterialDesignThemes.Wpf.Tests/RatingBarTests.cs
@@ -0,0 +1,420 @@
+using System.Globalization;
+using System.Windows.Data;
+using System.Windows.Media;
+using Xunit;
+
+namespace MaterialDesignThemes.Wpf.Tests;
+
+public class RatingBarTests
+{
+ [StaFact]
+ public void SetMin_ShouldCoerceToMax_WhenMinIsGreaterThanMax()
+ {
+ // Arrange
+ RatingBar ratingBar = new() { Min = 1, Max = 10 };
+
+ // Act
+ ratingBar.Min = 15;
+
+ // Assert
+ Assert.Equal(10, ratingBar.Min);
+ }
+
+ [StaFact]
+ public void SetMin_ShouldNotCoerceValue_WhenFractionalValuesAreDisabled()
+ {
+ // Arrange
+ RatingBar ratingBar = new() { Min = 1, Max = 10, Value = 5};
+
+ // Act
+ ratingBar.Min = 7;
+
+ // Assert
+ Assert.Equal(5, ratingBar.Value);
+ }
+
+ [StaFact]
+ public void SetMin_ShouldCoerceValue_WhenFractionalValuesAreEnabled()
+ {
+ // Arrange
+ RatingBar ratingBar = new() { Min = 1, Max = 10, Value = 5, ValueIncrements = 0.5 };
+
+ // Act
+ ratingBar.Min = 7;
+
+ // Assert
+ Assert.Equal(7, ratingBar.Value);
+ }
+
+ [StaFact]
+ public void SetMax_ShouldNotCoerceValue_WhenFractionalValuesAreDisabled()
+ {
+ // Arrange
+ RatingBar ratingBar = new() { Min = 1, Max = 10, Value = 5 };
+
+ // Act
+ ratingBar.Max = 3;
+
+ // Assert
+ Assert.Equal(5, ratingBar.Value);
+ }
+
+ [StaFact]
+ public void SetMax_ShouldCoerceValue_WhenFractionalValuesAreEnabled()
+ {
+ // Arrange
+ RatingBar ratingBar = new() { Min = 1, Max = 10, Value = 5, ValueIncrements = 0.5 };
+
+ // Act
+ ratingBar.Max = 3;
+
+ // Assert
+ Assert.Equal(3, ratingBar.Value);
+ }
+
+ [StaFact]
+ public void SetMax_ShouldCoerceToMin_WhenMaxIsLessThanMin()
+ {
+ // Arrange
+ RatingBar ratingBar = new() { Min = 1, Max = 10 };
+
+ // Act
+ ratingBar.Max = -5;
+
+ // Assert
+ Assert.Equal(1, ratingBar.Max);
+ }
+
+ [StaTheory]
+ [InlineData(-5, 1.0)]
+ [InlineData(5, 5.0)]
+ [InlineData(15, 10.0)]
+ [InlineData(1.2, 1.0)]
+ [InlineData(1.3, 1.5)]
+ [InlineData(1.7, 1.5)]
+ [InlineData(1.8, 2.0)]
+ [InlineData(2.2, 2.0)]
+ [InlineData(2.3, 2.5)]
+ public void SetValue_ShouldCoerceToCorrectMultipleAndStaysWithinBounds_WhenFractionalValuesAreEnabled(double valueToSet, double expectedValue)
+ {
+ // Arrange
+ RatingBar ratingBar = new() { Min = 1, Max = 10, ValueIncrements = 0.5 };
+
+ // Act
+ ratingBar.Value = valueToSet;
+
+ // Assert
+ Assert.Equal(expectedValue, ratingBar.Value);
+ }
+
+ [StaTheory]
+ [InlineData(-5, -5.0)]
+ [InlineData(5, 5.0)]
+ [InlineData(15, 15.0)]
+ [InlineData(1.2, 1.2)]
+ [InlineData(2.3, 2.3)]
+ public void SetValue_ShouldNotCoerceValue_WhenFractionalValuesAreDisabled(double valueToSet, double expectedValue)
+ {
+ // Arrange
+ RatingBar ratingBar = new() { Min = 1, Max = 10 };
+
+ // Act
+ ratingBar.Value = valueToSet;
+
+ // Assert
+ Assert.Equal(expectedValue, ratingBar.Value);
+ }
+
+ [Fact]
+ public void TextBlockForegroundConverter_ShouldReturnOriginalBrush_WhenValueIsEqualToButtonValue()
+ {
+ // Arrange
+ SolidColorBrush brush = Brushes.Red;
+ IMultiValueConverter converter = RatingBar.TextBlockForegroundConverter.Instance;
+ object[] values = Arrange_TextBlockForegroundConverterValues(brush, value: 1, buttonValue: 1);
+
+ // Act
+ var result = converter.Convert(values, typeof(Brush), null, CultureInfo.CurrentCulture) as Brush;
+
+ // Assert
+ Assert.Equal(brush, result);
+ }
+
+ [Fact]
+ public void TextBlockForegroundConverter_ShouldReturnOriginalBrush_WhenValueIsGreaterThanButtonValue()
+ {
+ // Arrange
+ SolidColorBrush brush = Brushes.Red;
+ IMultiValueConverter converter = RatingBar.TextBlockForegroundConverter.Instance;
+ object[] values = Arrange_TextBlockForegroundConverterValues(brush, value: 2, buttonValue: 1);
+
+ // Act
+ var result = converter.Convert(values, typeof(Brush), null, CultureInfo.CurrentCulture) as Brush;
+
+ // Assert
+ Assert.Equal(brush, result);
+ }
+
+ [Fact]
+ public void TextBlockForegroundConverter_ShouldReturnSemiTransparentBrush_WhenValueIsLessThanButtonValueMinusOne()
+ {
+ // Arrange
+ SolidColorBrush brush = Brushes.Red;
+ IMultiValueConverter converter = RatingBar.TextBlockForegroundConverter.Instance;
+ object[] values = Arrange_TextBlockForegroundConverterValues(brush, value: 0.5, buttonValue: 2);
+
+ // Act
+ var result = converter.Convert(values, typeof(Brush), null, CultureInfo.CurrentCulture) as Brush;
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ SolidColorBrush resultBrush = (SolidColorBrush)result!;
+ Assert.Equal(RatingBar.TextBlockForegroundConverter.SemiTransparent, resultBrush.Color.A);
+ }
+
+ [Fact]
+ public void TextBlockForegroundConverter_ShouldReturnHorizontalLinearGradientBrush_WhenValueIsBetweenButtonValueAndButtonValueMinusOne()
+ {
+ // Arrange
+ SolidColorBrush brush = Brushes.Red;
+ IMultiValueConverter converter = RatingBar.TextBlockForegroundConverter.Instance;
+ object[] values = Arrange_TextBlockForegroundConverterValues(brush, value: 1.5, buttonValue: 2, orientation: Orientation.Horizontal);
+
+ // Act
+ var result = converter.Convert(values, typeof(Brush), null, CultureInfo.CurrentCulture) as Brush;
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ LinearGradientBrush resultBrush = (LinearGradientBrush)result!;
+ Assert.Equal(new Point(0, 0.5), resultBrush.StartPoint);
+ Assert.Equal(new Point(1, 0.5), resultBrush.EndPoint);
+ }
+
+ [Fact]
+ public void TextBlockForegroundConverter_ShouldReturnVerticalLinearGradientBrush_WhenValueIsBetweenButtonValueAndButtonValueMinusOne()
+ {
+ // Arrange
+ SolidColorBrush brush = Brushes.Red;
+ IMultiValueConverter converter = RatingBar.TextBlockForegroundConverter.Instance;
+ object[] values = Arrange_TextBlockForegroundConverterValues(brush, value: 1.5, buttonValue: 2, orientation: Orientation.Vertical);
+
+ // Act
+ var result = converter.Convert(values, typeof(Brush), null, CultureInfo.CurrentCulture) as Brush;
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ LinearGradientBrush resultBrush = (LinearGradientBrush)result!;
+ Assert.Equal(new Point(0.5, 0), resultBrush.StartPoint);
+ Assert.Equal(new Point(0.5, 1), resultBrush.EndPoint);
+ }
+
+ [Fact]
+ public void TextBlockForegroundConverter_ShouldReturnFractionalGradientStops_WhenValueCovers10PercentOfButtonValue()
+ {
+ // Arrange
+ SolidColorBrush brush = Brushes.Red;
+ IMultiValueConverter converter = RatingBar.TextBlockForegroundConverter.Instance;
+ object[] values = Arrange_TextBlockForegroundConverterValues(brush, value: 1.1, buttonValue: 2);
+
+ // Act
+ var result = converter.Convert(values, typeof(Brush), null, CultureInfo.CurrentCulture) as Brush;
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ LinearGradientBrush resultBrush = (LinearGradientBrush)result!;
+ Assert.Equal(2, resultBrush.GradientStops.Count);
+ GradientStop stop1 = resultBrush.GradientStops[0];
+ GradientStop stop2 = resultBrush.GradientStops[1];
+ Assert.Equal(0.1, stop1.Offset, 10);
+ Assert.Equal(brush.Color, stop1.Color);
+ Assert.Equal(0.1, stop2.Offset, 10);
+ Assert.Equal(brush.Color.WithAlphaChannel(RatingBar.TextBlockForegroundConverter.SemiTransparent), stop2.Color);
+ }
+
+ [Fact]
+ public void TextBlockForegroundConverter_ShouldReturnFractionalGradientStops_WhenValueCovers42PercentOfButtonValue()
+ {
+ // Arrange
+ SolidColorBrush brush = Brushes.Red;
+ IMultiValueConverter converter = RatingBar.TextBlockForegroundConverter.Instance;
+ object[] values = Arrange_TextBlockForegroundConverterValues(brush, value: 1.42, buttonValue: 2);
+
+ // Act
+ var result = converter.Convert(values, typeof(Brush), null, CultureInfo.CurrentCulture) as Brush;
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ LinearGradientBrush resultBrush = (LinearGradientBrush)result!;
+ Assert.Equal(2, resultBrush.GradientStops.Count);
+ GradientStop stop1 = resultBrush.GradientStops[0];
+ GradientStop stop2 = resultBrush.GradientStops[1];
+ Assert.Equal(0.42, stop1.Offset, 10);
+ Assert.Equal(brush.Color, stop1.Color);
+ Assert.Equal(0.42, stop2.Offset, 10);
+ Assert.Equal(brush.Color.WithAlphaChannel(RatingBar.TextBlockForegroundConverter.SemiTransparent), stop2.Color);
+ }
+
+ [Fact]
+ public void TextBlockForegroundConverter_ShouldReturnFractionalGradientStops_WhenValueCovers87PercentOfButtonValue()
+ {
+ // Arrange
+ SolidColorBrush brush = Brushes.Red;
+ IMultiValueConverter converter = RatingBar.TextBlockForegroundConverter.Instance;
+ object[] values = Arrange_TextBlockForegroundConverterValues(brush, value: 1.87, buttonValue: 2);
+
+ // Act
+ var result = converter.Convert(values, typeof(Brush), null, CultureInfo.CurrentCulture) as Brush;
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ LinearGradientBrush resultBrush = (LinearGradientBrush)result!;
+ Assert.Equal(2, resultBrush.GradientStops.Count);
+ GradientStop stop1 = resultBrush.GradientStops[0];
+ GradientStop stop2 = resultBrush.GradientStops[1];
+ Assert.Equal(0.87, stop1.Offset, 10);
+ Assert.Equal(brush.Color, stop1.Color);
+ Assert.Equal(0.87, stop2.Offset, 10);
+ Assert.Equal(brush.Color.WithAlphaChannel(RatingBar.TextBlockForegroundConverter.SemiTransparent), stop2.Color);
+ }
+
+ private static object[] Arrange_TextBlockForegroundConverterValues(SolidColorBrush brush, double value, int buttonValue, Orientation orientation = Orientation.Horizontal) =>
+ new object[] { brush, orientation, value, buttonValue };
+
+ [Fact]
+ public void PreviewIndicatorTransformXConverter_ShouldCenterPreviewIndicator_WhenFractionalValuesAreDisabledAndOrientationIsHorizontal()
+ {
+ // Arrange
+ IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformXConverter.Instance;
+ object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Horizontal, false, 1, 1);
+
+ // Act
+ double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(40.0, result); // 50% of 100 minus 20/2
+ }
+
+ [Fact]
+ public void PreviewIndicatorTransformXConverter_ShouldOffsetPreviewIndicatorByPercentage_WhenFractionalValuesAreEnabledAndOrientationIsHorizontal()
+ {
+ // Arrange
+ IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformXConverter.Instance;
+ object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Horizontal, true, 1.25, 1);
+
+ // Act
+ double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(15.0, result); // 25% of 100 minus 20/2
+ }
+
+ [Fact]
+ public void PreviewIndicatorTransformXConverter_ShouldPlacePreviewIndicatorWithSmallMargin_WhenFractionalValuesAreDisabledAndOrientationIsVertical()
+ {
+ // Arrange
+ IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformXConverter.Instance;
+ object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Vertical, false, 1, 1);
+ double expectedValue = -20 - RatingBar.PreviewIndicatorTransformXConverter.Margin;
+
+ // Act
+ double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(expectedValue, result); // 100% of 20 minus fixed margin
+ }
+
+ [Fact]
+ public void PreviewIndicatorTransformXConverter_ShouldPlacePreviewIndicatorWithSmallMargin_WhenFractionalValuesAreEnabledAndOrientationIsVertical()
+ {
+ // Arrange
+ IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformXConverter.Instance;
+ object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Vertical, true, 1.25, 1);
+ double expectedValue = -20 - RatingBar.PreviewIndicatorTransformXConverter.Margin;
+
+ // Act
+ double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(expectedValue, result); // 100% of 20 minus fixed margin
+ }
+
+
+
+ private static object[] Arrange_PreviewIndicatorTransformXConverterValues(double ratingBarButtonActualWidth, double previewValueActualWidth, Orientation orientation, bool isFractionalValueEnabled, double previewValue, int buttonValue) =>
+ new object[] { ratingBarButtonActualWidth, previewValueActualWidth, orientation, isFractionalValueEnabled, previewValue, buttonValue };
+
+ [Fact]
+ public void PreviewIndicatorTransformYConverter_ShouldPlacePreviewIndicatorWithSmallMargin_WhenFractionalValuesAreDisabledAndOrientationIsHorizontal()
+ {
+ // Arrange
+ IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformYConverter.Instance;
+ object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Horizontal, false, 1, 1);
+ double expectedValue = -20 - RatingBar.PreviewIndicatorTransformYConverter.Margin;
+
+ // Act
+ double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(expectedValue, result); // 100% of 20 minus fixed margin
+ }
+
+ [Fact]
+ public void PreviewIndicatorTransformYConverter_ShouldPlacePreviewIndicatorWithSmallMargin_WhenFractionalValuesAreEnabledAndOrientationIsHorizontal()
+ {
+ // Arrange
+ IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformYConverter.Instance;
+ object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Horizontal, true, 1.25, 1);
+ double expectedValue = -20 - RatingBar.PreviewIndicatorTransformYConverter.Margin;
+
+ // Act
+ double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(expectedValue, result); // 100% of 20 minus fixed margin
+ }
+
+ [Fact]
+ public void PreviewIndicatorTransformYConverter_ShouldCenterPreviewIndicator_WhenFractionalValuesAreDisabledAndOrientationIsVertical()
+ {
+ // Arrange
+ IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformYConverter.Instance;
+ object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Vertical, false, 1, 1);
+
+ // Act
+ double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(40.0, result); // 50% of 100 minus 20/2
+ }
+
+ [Fact]
+ public void PreviewIndicatorTransformYConverter_ShouldPreviewIndicatorByPercentage_WhenFractionalValuesAreEnabledAndOrientationIsVertical()
+ {
+ // Arrange
+ IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformYConverter.Instance;
+ object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Vertical, true, 1.25, 1);
+
+ // Act
+ double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(15.0, result); // 25% of 100 minus 20/2
+ }
+
+ private static object[] Arrange_PreviewIndicatorTransformYConverterValues(double ratingBarButtonActualHeight, double previewValueActualHeight, Orientation orientation, bool isFractionalValueEnabled, double previewValue, int buttonValue) =>
+ new object[] { ratingBarButtonActualHeight, previewValueActualHeight, orientation, isFractionalValueEnabled, previewValue, buttonValue };
+}
+
+internal static class ColorExtensions
+{
+ public static Color WithAlphaChannel(this Color color, byte alphaChannel)
+ => Color.FromArgb(alphaChannel, color.R, color.G, color.B);
+}
\ No newline at end of file
diff --git a/MaterialDesignThemes.Wpf/RatingBar.cs b/MaterialDesignThemes.Wpf/RatingBar.cs
index ef42f924c2..b1b75452ac 100644
--- a/MaterialDesignThemes.Wpf/RatingBar.cs
+++ b/MaterialDesignThemes.Wpf/RatingBar.cs
@@ -1,7 +1,7 @@
using System.Collections.ObjectModel;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
+using System.Globalization;
+using System.Windows.Data;
+using System.Windows.Media;
namespace MaterialDesignThemes.Wpf
{
@@ -25,16 +25,32 @@ public RatingBar()
{
CommandBindings.Add(new CommandBinding(SelectRatingCommand, SelectItemHandler));
_ratingButtons = new ReadOnlyObservableCollection(_ratingButtonsInternal);
+ MouseLeave += RatingBar_MouseLeave;
}
private void SelectItemHandler(object sender, ExecutedRoutedEventArgs executedRoutedEventArgs)
{
if (executedRoutedEventArgs.Parameter is int && !IsReadOnly)
- Value = (int)executedRoutedEventArgs.Parameter;
+ {
+ if (!IsFractionalValueEnabled)
+ {
+ Value = (int)executedRoutedEventArgs.Parameter;
+ return;
+ }
+ Value = GetValueAtMousePosition((RatingBarButton)executedRoutedEventArgs.OriginalSource);
+ }
+ }
+
+ private double GetValueAtMousePosition(RatingBarButton ratingBarButton)
+ {
+ // Get mouse offset inside source
+ Point p = Mouse.GetPosition(ratingBarButton);
+ double percentSelected = Orientation == Orientation.Horizontal ? p.X / ratingBarButton.ActualWidth : p.Y / ratingBarButton.ActualHeight;
+ return ratingBarButton.Value - 1 + percentSelected;
}
public static readonly DependencyProperty MinProperty = DependencyProperty.Register(
- nameof(Min), typeof(int), typeof(RatingBar), new PropertyMetadata(1, MinPropertyChangedCallback));
+ nameof(Min), typeof(int), typeof(RatingBar), new PropertyMetadata(1, MinPropertyChangedCallback, MinPropertyCoerceValueCallback));
public int Min
{
@@ -43,7 +59,7 @@ public int Min
}
public static readonly DependencyProperty MaxProperty = DependencyProperty.Register(
- nameof(Max), typeof(int), typeof(RatingBar), new PropertyMetadata(5, MaxPropertyChangedCallback));
+ nameof(Max), typeof(int), typeof(RatingBar), new PropertyMetadata(5, MaxPropertyChangedCallback, MaxPropertyCoerceValueCallback));
public int Max
{
@@ -51,20 +67,117 @@ public int Max
set => SetValue(MaxProperty, value);
}
+ private static readonly DependencyPropertyKey IsFractionalValueEnabledPropertyKey = DependencyProperty.RegisterReadOnly(
+ nameof(IsFractionalValueEnabled), typeof(bool), typeof(RatingBar), new PropertyMetadata(false));
+
+ internal static readonly DependencyProperty IsFractionalValueEnabledProperty =
+ IsFractionalValueEnabledPropertyKey.DependencyProperty;
+
+ internal bool IsFractionalValueEnabled
+ {
+ get => (bool)GetValue(IsFractionalValueEnabledProperty);
+ private set => SetValue(IsFractionalValueEnabledPropertyKey, value);
+ }
+
+ public static readonly DependencyProperty ValueIncrementsProperty = DependencyProperty.Register(
+ nameof(ValueIncrements), typeof(double), typeof(RatingBar), new PropertyMetadata(1.0, ValueIncrementsPropertyChangedCallback, ValueIncrementsCoerceValueCallback));
+
+ private static void ValueIncrementsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var ratingBar = (RatingBar)d;
+ ratingBar.IsFractionalValueEnabled = Math.Abs(ratingBar.ValueIncrements - 1.0) > 1e-10;
+ ratingBar.RebuildButtons();
+ }
+
+ private static object ValueIncrementsCoerceValueCallback(DependencyObject d, object baseValue)
+ => Math.Max(double.Epsilon, Math.Min(1.0, (double)baseValue));
+
+ ///
+ /// Gets or sets the value increments. Set to a value between 0.0 and 1.0 (both exclusive) to enable fractional values. Default value is 1.0 (i.e. fractional values disabled)
+ ///
+ public double ValueIncrements
+ {
+ get { return (double) GetValue(ValueIncrementsProperty); }
+ set { SetValue(ValueIncrementsProperty, value); }
+ }
+
+ public static readonly DependencyProperty IsPreviewValueEnabledProperty = DependencyProperty.Register(
+ nameof(IsPreviewValueEnabled), typeof(bool), typeof(RatingBar), new PropertyMetadata(false));
+
+ public bool IsPreviewValueEnabled
+ {
+ get { return (bool) GetValue(IsPreviewValueEnabledProperty); }
+ set { SetValue(IsPreviewValueEnabledProperty, value); }
+ }
+
+ private static readonly DependencyPropertyKey PreviewValuePropertyKey = DependencyProperty.RegisterReadOnly(
+ nameof(PreviewValue), typeof(double?), typeof(RatingBar), new PropertyMetadata(null, null, PreviewValuePropertyCoerceValueCallback));
+
+ private static object? PreviewValuePropertyCoerceValueCallback(DependencyObject d, object? baseValue)
+ {
+ if (baseValue == null)
+ return null;
+
+ var ratingBar = (RatingBar)d;
+ if (baseValue is double value)
+ {
+ if (!ratingBar.IsFractionalValueEnabled)
+ value = Math.Ceiling(value);
+ return ratingBar.CoerceToValidIncrement(value);
+ }
+ return (double)ratingBar.Min;
+ }
+
+ internal static readonly DependencyProperty PreviewValueProperty =
+ PreviewValuePropertyKey.DependencyProperty;
+
+ internal double? PreviewValue
+ {
+ get => (double?)GetValue(PreviewValueProperty);
+ private set => SetValue(PreviewValuePropertyKey, value);
+ }
+
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
- nameof(Value), typeof(int), typeof(RatingBar), new PropertyMetadata(0, ValuePropertyChangedCallback));
+ nameof(Value), typeof(double), typeof(RatingBar), new PropertyMetadata(0.0, ValuePropertyChangedCallback, ValuePropertyCoerceValueCallback));
private static void ValuePropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var ratingBar = (RatingBar)dependencyObject;
foreach (var button in ratingBar.RatingButtons)
- button.IsWithinSelectedValue = button.Value <= (int)dependencyPropertyChangedEventArgs.NewValue;
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ // The property being set here is no longer used. If the RatingBarButton (and the DP) was not public I would have just removed it.
+ button.IsWithinSelectedValue = button.Value <= (double)dependencyPropertyChangedEventArgs.NewValue;
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
OnValueChanged(ratingBar, dependencyPropertyChangedEventArgs);
}
- public int Value
+ private static object ValuePropertyCoerceValueCallback(DependencyObject d, object baseValue)
{
- get => (int)GetValue(ValueProperty);
+ var ratingBar = (RatingBar) d;
+
+ // If factional values are disabled we don't do any coercion. This maintains back-compat where coercion was not applied and Value could be outside of Min/Max range.
+ if (!ratingBar.IsFractionalValueEnabled)
+ return baseValue;
+
+ if (baseValue is double value)
+ {
+ return ratingBar.CoerceToValidIncrement(value);
+ }
+ return (double)ratingBar.Min;
+ }
+
+ private double CoerceToValidIncrement(double value)
+ {
+ // Coerce the value into a multiple of ValueIncrements and within the bounds.
+ double valueInCorrectMultiple = Math.Round(value / ValueIncrements, MidpointRounding.AwayFromZero) * ValueIncrements;
+ return Math.Min(Max, Math.Max(Min, valueInCorrectMultiple));
+ }
+
+ public double Value
+ {
+ get => (double)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
@@ -72,10 +185,10 @@ public int Value
EventManager.RegisterRoutedEvent(
nameof(Value),
RoutingStrategy.Bubble,
- typeof(RoutedPropertyChangedEventHandler),
+ typeof(RoutedPropertyChangedEventHandler),
typeof(RatingBar));
- public event RoutedPropertyChangedEventHandler ValueChanged
+ public event RoutedPropertyChangedEventHandler ValueChanged
{
add => AddHandler(ValueChangedEvent, value);
remove => RemoveHandler(ValueChangedEvent, value);
@@ -85,9 +198,9 @@ private static void OnValueChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var instance = (RatingBar)d;
- var args = new RoutedPropertyChangedEventArgs(
- (int)e.OldValue,
- (int)e.NewValue)
+ var args = new RoutedPropertyChangedEventArgs(
+ (double)e.OldValue,
+ (double)e.NewValue)
{ RoutedEvent = ValueChangedEvent };
instance.RaiseEvent(args);
}
@@ -141,36 +254,211 @@ public bool IsReadOnly
private static void MaxPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
- ((RatingBar)dependencyObject).RebuildButtons();
+ var ratingBar = (RatingBar)dependencyObject;
+ ratingBar.CoerceValue(ValueProperty);
+ ratingBar.RebuildButtons();
+ }
+
+ private static object MinPropertyCoerceValueCallback(DependencyObject d, object baseValue)
+ {
+ var ratingBar = (RatingBar)d;
+ return Math.Min((int)baseValue, ratingBar.Max);
}
private static void MinPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
- ((RatingBar)dependencyObject).RebuildButtons();
+ var ratingBar = (RatingBar)dependencyObject;
+ ratingBar.CoerceValue(ValueProperty);
+ ratingBar.RebuildButtons();
+ }
+
+ private static object MaxPropertyCoerceValueCallback(DependencyObject d, object baseValue)
+ {
+ var ratingBar = (RatingBar)d;
+ return Math.Max((int)baseValue, ratingBar.Min);
}
private void RebuildButtons()
{
+ foreach (var ratingBarButton in _ratingButtonsInternal)
+ {
+ ratingBarButton.MouseMove -= RatingBarButton_MouseMove;
+ }
_ratingButtonsInternal.Clear();
- for (var i = Min; i <= Max; i++)
+
+ // When fractional values are enabled, the first rating button represents the value Min when not selected at all and Min+1 when fully selected;
+ // thus we start with the value Min+1 for the values of the rating buttons.
+ int start = IsFractionalValueEnabled ? Min + 1 : Min;
+ for (int i = start; i <= Max; i++)
{
- _ratingButtonsInternal.Add(new RatingBarButton
+ var ratingBarButton = new RatingBarButton
{
Content = i,
ContentTemplate = ValueItemTemplate,
ContentTemplateSelector = ValueItemTemplateSelector,
+#pragma warning disable CS0618 // Type or member is obsolete
IsWithinSelectedValue = i <= Value,
+#pragma warning restore CS0618 // Type or member is obsolete
Style = ValueItemContainerButtonStyle,
Value = i,
- });
+ };
+ ratingBarButton.MouseMove += RatingBarButton_MouseMove;
+ _ratingButtonsInternal.Add(ratingBarButton);
}
}
+ private void RatingBar_MouseLeave(object sender, MouseEventArgs e) => PreviewValue = null;
+
+ private void RatingBarButton_MouseMove(object sender, MouseEventArgs e)
+ {
+ if (!IsPreviewValueEnabled)
+ return;
+
+ var ratingBarButton = (RatingBarButton) sender;
+ PreviewValue = GetValueAtMousePosition(ratingBarButton);
+ }
+
public override void OnApplyTemplate()
{
RebuildButtons();
base.OnApplyTemplate();
}
+
+ internal class TextBlockForegroundConverter : IMultiValueConverter
+ {
+ internal static byte SemiTransparent => 0x42; // ~26% opacity
+
+ public static TextBlockForegroundConverter Instance { get; } = new();
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values?.Length == 4
+ && values[0] is SolidColorBrush brush
+ && values[1] is Orientation orientation
+ && values[2] is double value
+ && values[3] is int buttonValue)
+ {
+ if (value >= buttonValue)
+ return brush;
+
+ var originalColor = brush.Color;
+ var semiTransparent = Color.FromArgb(SemiTransparent, brush.Color.R, brush.Color.G, brush.Color.B);
+
+ if (value > buttonValue - 1.0)
+ {
+ double offset = value - buttonValue + 1;
+ return new LinearGradientBrush
+ {
+ StartPoint = orientation == Orientation.Horizontal ? new Point(0, 0.5) : new Point(0.5, 0),
+ EndPoint = orientation == Orientation.Horizontal ? new Point(1, 0.5) : new Point(0.5, 1),
+ GradientStops = new()
+ {
+ new GradientStop {Color = originalColor, Offset = offset},
+ new GradientStop {Color = semiTransparent, Offset = offset}
+ }
+ };
+ }
+ return new SolidColorBrush(semiTransparent);
+ }
+
+ // This should never happen (returning actual brush to avoid the compilers squiggly line warning)
+ return Brushes.Transparent;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
+
+ internal class PreviewIndicatorTransformXConverter : IMultiValueConverter
+ {
+ public static PreviewIndicatorTransformXConverter Instance { get; } = new();
+
+ internal static double Margin => 2.0;
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values.Length >= 6
+ && values[0] is double ratingBarButtonActualWidth
+ && values[1] is double previewValueActualWidth
+ && values[2] is Orientation ratingBarOrientation
+ && values[3] is bool isFractionalValueEnabled
+ && values[4] is double previewValue
+ && values[5] is int ratingButtonValue)
+ {
+ if (!isFractionalValueEnabled)
+ {
+ return ratingBarOrientation switch
+ {
+ Orientation.Horizontal => (ratingBarButtonActualWidth - previewValueActualWidth) / 2,
+ Orientation.Vertical => -previewValueActualWidth - Margin,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ // Special handling of edge cases due to the inaccuracy of how double values are stored
+ double percent = previewValue % 1;
+ if (Math.Abs(ratingButtonValue - previewValue) <= double.Epsilon)
+ percent = 1.0;
+ else if (percent <= double.Epsilon)
+ percent = 0.0;
+
+ return ratingBarOrientation switch
+ {
+ Orientation.Horizontal => percent * ratingBarButtonActualWidth - (previewValueActualWidth / 2),
+ Orientation.Vertical => -previewValueActualWidth - Margin,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+ return 1.0;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
+
+ internal class PreviewIndicatorTransformYConverter : IMultiValueConverter
+ {
+ public static PreviewIndicatorTransformYConverter Instance { get; } = new();
+
+ internal static double Margin => 2.0;
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values.Length >= 6
+ && values[0] is double ratingBarButtonActualHeight
+ && values[1] is double previewValueActualHeight
+ && values[2] is Orientation ratingBarOrientation
+ && values[3] is bool isFractionalValueEnabled
+ && values[4] is double previewValue
+ && values[5] is int ratingButtonValue)
+ {
+ if (!isFractionalValueEnabled)
+ {
+ return ratingBarOrientation switch
+ {
+ Orientation.Horizontal => -previewValueActualHeight - Margin,
+ Orientation.Vertical => (ratingBarButtonActualHeight - previewValueActualHeight) / 2,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ // Special handling of edge cases due to the inaccuracy of how double values are stored
+ double percent = previewValue % 1;
+ if (Math.Abs(ratingButtonValue - previewValue) <= double.Epsilon)
+ percent = 1.0;
+ else if (percent <= double.Epsilon)
+ percent = 0.0;
+
+ return ratingBarOrientation switch
+ {
+ Orientation.Horizontal => -previewValueActualHeight - Margin,
+ Orientation.Vertical => percent * ratingBarButtonActualHeight - (previewValueActualHeight / 2),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+ return 1.0;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
}
}
diff --git a/MaterialDesignThemes.Wpf/RatingBarButton.cs b/MaterialDesignThemes.Wpf/RatingBarButton.cs
index 7b75352f58..fd805bcc26 100644
--- a/MaterialDesignThemes.Wpf/RatingBarButton.cs
+++ b/MaterialDesignThemes.Wpf/RatingBarButton.cs
@@ -1,41 +1,39 @@
-using System.Windows;
-using System.Windows.Controls.Primitives;
+namespace MaterialDesignThemes.Wpf;
-namespace MaterialDesignThemes.Wpf
+public class RatingBarButton : ButtonBase
{
- public class RatingBarButton : ButtonBase
+ static RatingBarButton()
{
- static RatingBarButton()
- {
- DefaultStyleKeyProperty.OverrideMetadata(typeof(RatingBarButton), new FrameworkPropertyMetadata(typeof(RatingBarButton)));
- }
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(RatingBarButton), new FrameworkPropertyMetadata(typeof(RatingBarButton)));
+ }
- private static readonly DependencyPropertyKey ValuePropertyKey =
- DependencyProperty.RegisterReadOnly(
- "Value", typeof(int), typeof(RatingBarButton),
- new PropertyMetadata(default(int)));
+ private static readonly DependencyPropertyKey ValuePropertyKey =
+ DependencyProperty.RegisterReadOnly(
+ "Value", typeof(int), typeof(RatingBarButton),
+ new PropertyMetadata(default(int)));
- public static readonly DependencyProperty ValueProperty =
- ValuePropertyKey.DependencyProperty;
+ public static readonly DependencyProperty ValueProperty =
+ ValuePropertyKey.DependencyProperty;
- public int Value
- {
- get { return (int)GetValue(ValueProperty); }
- internal set { SetValue(ValuePropertyKey, value); }
- }
+ public int Value
+ {
+ get => (int)GetValue(ValueProperty);
+ internal set => SetValue(ValuePropertyKey, value);
+ }
- private static readonly DependencyPropertyKey IsWithinValuePropertyKey =
- DependencyProperty.RegisterReadOnly(
- "IsWithinSelectedValue", typeof(bool), typeof(RatingBarButton),
- new PropertyMetadata(default(bool)));
+ private static readonly DependencyPropertyKey IsWithinValuePropertyKey =
+ DependencyProperty.RegisterReadOnly(
+ "IsWithinSelectedValue", typeof(bool), typeof(RatingBarButton),
+ new PropertyMetadata(default(bool)));
- public static readonly DependencyProperty IsWithinSelectedValueProperty =
- IsWithinValuePropertyKey.DependencyProperty;
+ [Obsolete("This will be removed in a future version")]
+ public static readonly DependencyProperty IsWithinSelectedValueProperty =
+ IsWithinValuePropertyKey.DependencyProperty;
- public bool IsWithinSelectedValue
- {
- get => (bool)GetValue(IsWithinSelectedValueProperty);
- internal set => SetValue(IsWithinValuePropertyKey, value);
- }
+ [Obsolete("This will be removed in a future version")]
+ public bool IsWithinSelectedValue
+ {
+ get => (bool)GetValue(IsWithinSelectedValueProperty);
+ internal set => SetValue(IsWithinValuePropertyKey, value);
}
}
\ No newline at end of file
diff --git a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.RatingBar.xaml b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.RatingBar.xaml
index bb26a98e07..d012ecad09 100644
--- a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.RatingBar.xaml
+++ b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.RatingBar.xaml
@@ -1,12 +1,14 @@
-
+ xmlns:system="clr-namespace:System;assembly=mscorlib"
+ xmlns:converters="clr-namespace:MaterialDesignThemes.Wpf.Converters">