Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
08a8782
POC of SplitButton leveraging PopupBox
nicolaihenriksen Nov 23, 2023
902f3db
Fixing drop-shadow issue and Click event issue
nicolaihenriksen Nov 27, 2023
322cade
Fix indentation
nicolaihenriksen Nov 27, 2023
153fdfc
Removed the need for RightButtonMargin DP
nicolaihenriksen Nov 28, 2023
f76e229
Add style variations for SplitButton
nicolaihenriksen Dec 6, 2023
0f5d2cb
Added SplitButton to the MD3 demo app
nicolaihenriksen Dec 6, 2023
7ba11b6
Reset launchSettings.json back to its original values
nicolaihenriksen Dec 6, 2023
b30a485
Some minor cleanup and starting on UI Tests
Keboo Dec 7, 2023
36bf3eb
Add SplitButton.SplitContent and its relatives
nicolaihenriksen Dec 7, 2023
5a888c2
Revert cleanup as this introduces bugs
nicolaihenriksen Dec 8, 2023
4ba8199
Map SplitButton.Command and others into the nested Button
nicolaihenriksen Dec 8, 2023
1e72512
Add delay to allow the LeftClick to be registered before the assertion
nicolaihenriksen Dec 8, 2023
15f81e7
Avoid double opacity on PopupBox content when SplitButton is disabled
nicolaihenriksen Dec 8, 2023
216122f
Add UI test for command binding
nicolaihenriksen Dec 8, 2023
41934ea
Adding UI test for CanExecute=False on the Command binding
nicolaihenriksen Dec 8, 2023
22f1034
Revert launchSettings.json changes
nicolaihenriksen Dec 8, 2023
2a25021
Revert TestBase changes
nicolaihenriksen Dec 8, 2023
8e2f074
Revert changes in TestBase
nicolaihenriksen Dec 8, 2023
ef55b00
Add slight delays to make test run green in the pipeline
nicolaihenriksen Dec 8, 2023
d32219a
Minor clean up on tests
Keboo Dec 8, 2023
c9f5b4a
Fully qualifying debugger.
Keboo Dec 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 286 additions & 4 deletions MainDemo.Wpf/Buttons.xaml

Large diffs are not rendered by default.

284 changes: 283 additions & 1 deletion MaterialDesign3.Demo.Wpf/Buttons.xaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<UserControl x:Class="MaterialDesignThemes.UITests.Samples.SplitButton.SplitButtonWithCommandBinding"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MaterialDesignThemes.UITests.Samples.SplitButton"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type={x:Type local:SplitButtonWithCommandBindingViewModel}, IsDesignTimeCreatable= False}">
<StackPanel>
<materialDesign:SplitButton Command="{Binding LeftSideButtonClickedCommand}"
Content="Split Button" />
</StackPanel>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MaterialDesignThemes.UITests.Samples.SplitButton;

/// <summary>
/// Interaction logic for SplitButtonWithCommandBinding.xaml
/// </summary>
public partial class SplitButtonWithCommandBinding
{
private SplitButtonWithCommandBindingViewModel ViewModel { get; }

public bool CommandInvoked => ViewModel.CommandInvoked;

public bool CommandCanExecute
{
get => ViewModel.CommandCanExecute;
set => ViewModel.CommandCanExecute = value;
}

public SplitButtonWithCommandBinding()
{
DataContext = ViewModel = new SplitButtonWithCommandBindingViewModel();
InitializeComponent();
}
}

public partial class SplitButtonWithCommandBindingViewModel : ObservableObject
{
public bool CommandInvoked { get; private set; }

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(LeftSideButtonClickedCommand))]
private bool _commandCanExecute = true;

[RelayCommand(CanExecute = nameof(CommandCanExecute))]
private void LeftSideButtonClicked() => CommandInvoked = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Window x:Class="MaterialDesignThemes.UITests.Samples.SplitButton.SplitButtonWithCommandBindingWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MaterialDesignThemes.UITests.Samples.SplitButton"
mc:Ignorable="d"
Title="SplitButtonWithCommandBindingWindow" Height="450" Width="800">
<local:SplitButtonWithCommandBinding />
</Window>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace MaterialDesignThemes.UITests.Samples.SplitButton;

/// <summary>
/// Interaction logic for SplitButtonWithCommandBindingWindow.xaml
/// </summary>
public partial class SplitButtonWithCommandBindingWindow
{
public SplitButtonWithCommandBindingWindow()
{
InitializeComponent();
}
}
19 changes: 8 additions & 11 deletions MaterialDesignThemes.UITests/TestBase.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Media;

[assembly: CollectionBehavior(DisableTestParallelization = true)]
[assembly: GenerateHelpers(typeof(SmartHint))]
[assembly: GenerateHelpers(typeof(TimePicker))]
[assembly: GenerateHelpers(typeof(DrawerHost))]
[assembly: GenerateHelpers(typeof(AutoSuggestBox))]
[assembly: GenerateHelpers(typeof(ColorPicker))]
[assembly: GenerateHelpers(typeof(DialogHost))]
[assembly: GenerateHelpers(typeof(AutoSuggestBox))]
[assembly: GenerateHelpers(typeof(DrawerHost))]
[assembly: GenerateHelpers(typeof(PopupBox))]
[assembly: GenerateHelpers(typeof(SmartHint))]
[assembly: GenerateHelpers(typeof(TimePicker))]
[assembly: GenerateHelpers(typeof(TreeListView))]
[assembly: GenerateHelpers(typeof(TreeListViewItem))]

namespace MaterialDesignThemes.UITests;

public abstract class TestBase : IAsyncLifetime
public abstract class TestBase(ITestOutputHelper output) : IAsyncLifetime
{
protected bool AttachedDebuggerToRemoteProcess { get; set; } = true;
protected ITestOutputHelper Output { get; }
protected ITestOutputHelper Output { get; } = output ?? throw new ArgumentNullException(nameof(output));

[NotNull]
protected IApp? App { get; set; }

public TestBase(ITestOutputHelper output)
=> Output = output ?? throw new ArgumentNullException(nameof(output));

protected async Task<Color> GetThemeColor(string name)
{
IResource resource = await App.GetResource(name);
Expand All @@ -51,7 +48,7 @@ public async Task InitializeAsync() =>
App = await XamlTest.App.StartRemote(new AppOptions
{
#if !DEBUG
MinimizeOtherWindows = !Debugger.IsAttached,
MinimizeOtherWindows = !System.Diagnostics.Debugger.IsAttached,
#endif
AllowVisualStudioDebuggerAttach = AttachedDebuggerToRemoteProcess,
LogMessage = Output.WriteLine
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Windows.Media;
using MaterialDesignThemes.UITests.Samples.PopupBox;

namespace MaterialDesignThemes.UITests.WPF.PopupBox;
namespace MaterialDesignThemes.UITests.WPF.PopupBoxes;

public class PopupBoxTests : TestBase
{
Expand All @@ -20,7 +20,7 @@ public async Task PopupBox_WithElevation_AppliesElevationToNestedCard(Elevation
await using var recorder = new TestRecorder(App);

//Arrange
IVisualElement<Wpf.PopupBox> popupBox = await LoadXaml<Wpf.PopupBox>($@"
IVisualElement<PopupBox> popupBox = await LoadXaml<PopupBox>($@"
<materialDesign:PopupBox VerticalAlignment=""Top""
PopupElevation=""{elevation}"">
<StackPanel>
Expand All @@ -46,7 +46,7 @@ public async Task PopupBox_WithContentTemplateSelector_ChangesContent()
IVisualElement grid = (await LoadUserControl<PopupBoxWithTemplateSelector>());

IVisualElement<Button> button = await grid.GetElement<Button>();
IVisualElement<Wpf.PopupBox> popupBox = await grid.GetElement<Wpf.PopupBox>();
IVisualElement<PopupBox> popupBox = await grid.GetElement<PopupBox>();


// Assert
Expand Down
155 changes: 155 additions & 0 deletions MaterialDesignThemes.UITests/WPF/SplitButtons/SplitButtonTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using MaterialDesignThemes.UITests.Samples.SplitButton;

[assembly:GenerateHelpers(typeof(SplitButtonWithCommandBinding))]

namespace MaterialDesignThemes.UITests.WPF.SplitButtons;


public class SplitButtonTests : TestBase
{
public SplitButtonTests(ITestOutputHelper output)
: base(output)
{
}

[Fact]
public async Task SplitButton_ClickingSplitButton_ShowsPopup()
{
await using var recorder = new TestRecorder(App);

//Arrange
IVisualElement<SplitButton> splitButton = await LoadXaml<SplitButton>("""
<materialDesign:SplitButton Content="Split Button" VerticalAlignment="Center" HorizontalAlignment="Center">
<materialDesign:SplitButton.PopupContent>
<TextBlock x:Name="PopupContent" Text="Popup Content"/>
</materialDesign:SplitButton.PopupContent>
</materialDesign:SplitButton>
""");

IVisualElement<PopupBox> popupBox = await splitButton.GetElement<PopupBox>();
IVisualElement<TextBlock> popupContent = await popupBox.GetElement<TextBlock>("PopupContent");

//Act
await popupBox.LeftClick();

// Assert
await Wait.For(async () => await popupBox.GetIsPopupOpen());
await Wait.For(async () => await popupContent.GetIsVisible());

recorder.Success();
}

[Fact]
public async Task SplitButton_RegisterForClick_RaisesEvent()
{
await using var recorder = new TestRecorder(App);

//Arrange
IVisualElement<SplitButton> splitButton = await LoadXaml<SplitButton>("""
<materialDesign:SplitButton Content="Split Button" VerticalAlignment="Center" HorizontalAlignment="Center">
<materialDesign:SplitButton.PopupContent>
<TextBlock x:Name="PopupContent" Text="Popup Content"/>
</materialDesign:SplitButton.PopupContent>
</materialDesign:SplitButton>
""");

IEventRegistration clickEvent = await splitButton.RegisterForEvent(ButtonBase.ClickEvent.Name);

IVisualElement<Button> leftButton = await splitButton.GetElement<Button>("PART_LeftButton");
IVisualElement<PopupBox> popupBox = await splitButton.GetElement<PopupBox>();

//Act
await leftButton.LeftClick();
await Task.Delay(50);
int leftButtonCount = (await clickEvent.GetInvocations()).Count;
await popupBox.LeftClick();
await Task.Delay(50);
int rightButtonCount = (await clickEvent.GetInvocations()).Count;

// Assert
Assert.Equal(1, leftButtonCount);
//NB: The popup box button should only show the popup not trigger the click event
Assert.Equal(1, rightButtonCount);

recorder.Success();
}

[Fact]
public async Task SplitButton_WithButtonInPopup_CanBeInvoked()
{
await using var recorder = new TestRecorder(App);

//Arrange
IVisualElement<SplitButton> splitButton = await LoadXaml<SplitButton>("""
<materialDesign:SplitButton Content="Split Button" VerticalAlignment="Center" HorizontalAlignment="Center">
<materialDesign:SplitButton.PopupContent>
<Button x:Name="PopupContent" Content="Popup Content" />
</materialDesign:SplitButton.PopupContent>
</materialDesign:SplitButton>
""");

IVisualElement<Button> popupContent = await splitButton.GetElement<Button>("PopupContent");
IVisualElement<PopupBox> popupBox = await splitButton.GetElement<PopupBox>();

IEventRegistration clickEvent = await popupContent.RegisterForEvent(ButtonBase.ClickEvent.Name);

//Act
await popupBox.LeftClick();
//NB: give the popup some time to show
await Wait.For(async () => await popupContent.GetIsVisible());
await Wait.For(async () => await popupContent.GetActualHeight() > 10);
await popupContent.LeftClick();
await Task.Delay(50);

// Assert
var invocations = await clickEvent.GetInvocations();
Assert.Single(invocations);

recorder.Success();
}

[Fact]
public async Task SplitButton_RegisterCommandBinding_InvokesCommand()
{
await using var recorder = new TestRecorder(App);

//Arrange
await App.InitializeWithMaterialDesign();
IWindow window = await App.CreateWindow<SplitButtonWithCommandBindingWindow>();
IVisualElement<SplitButtonWithCommandBinding> userControl = await window.GetElement<SplitButtonWithCommandBinding>();
IVisualElement<SplitButton> splitButton = await userControl.GetElement<SplitButton>();

Assert.False(await userControl.GetCommandInvoked());

//Act
await splitButton.LeftClick();
await Task.Delay(50);

// Assert
Assert.True(await userControl.GetCommandInvoked());

recorder.Success();
}

[Fact]
public async Task SplitButton_CommandCanExecuteFalse_DisablesButton()
{
await using var recorder = new TestRecorder(App);

//Arrange
await App.InitializeWithMaterialDesign();
IWindow window = await App.CreateWindow<SplitButtonWithCommandBindingWindow>();
IVisualElement<SplitButtonWithCommandBinding> userControl = await window.GetElement<SplitButtonWithCommandBinding>();
IVisualElement<SplitButton> splitButton = await userControl.GetElement<SplitButton>();

Assert.True(await splitButton.GetIsEnabled());

//Act
await userControl.SetProperty(nameof(SplitButtonWithCommandBinding.CommandCanExecute), false);

// Assert
Assert.False(await splitButton.GetIsEnabled());

recorder.Success();
}
}
28 changes: 28 additions & 0 deletions MaterialDesignThemes.Wpf/Converters/CornerRadiusCloneConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Globalization;
using System.Windows.Data;

namespace MaterialDesignThemes.Wpf.Converters;

public class CornerRadiusCloneConverter : IValueConverter
{
public double? FixedTopLeft { get; set; }
public double? FixedTopRight { get; set; }
public double? FixedBottomLeft { get; set; }
public double? FixedBottomRight { get; set; }

public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is CornerRadius cornerRadius)
{
return new CornerRadius(
FixedTopLeft ?? cornerRadius.TopLeft,
FixedTopRight ?? cornerRadius.TopRight,
FixedBottomRight ?? cornerRadius.BottomRight,
FixedBottomLeft ?? cornerRadius.BottomLeft);
}
return new CornerRadius();
}

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
13 changes: 13 additions & 0 deletions MaterialDesignThemes.Wpf/Converters/FirstNonNullConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Globalization;
using System.Windows.Data;

namespace MaterialDesignThemes.Wpf.Converters;

public class FirstNonNullConverter : IMultiValueConverter
{
public object? Convert(object?[]? values, Type targetType, object? parameter, CultureInfo culture)
=> values?.FirstOrDefault(v => v is not null);

public object?[] ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
9 changes: 9 additions & 0 deletions MaterialDesignThemes.Wpf/PopupBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,15 @@ public PopupBoxPlacementMode PlacementMode
set => SetValue(PlacementModeProperty, value);
}

internal static readonly DependencyProperty PlacementTargetProperty = DependencyProperty.Register(
nameof(PlacementTarget), typeof(UIElement), typeof(PopupBox), new PropertyMetadata(default(UIElement)));

internal UIElement? PlacementTarget
{
get => (UIElement?)GetValue(PlacementTargetProperty);
set => SetValue(PlacementTargetProperty, value);
}

public static readonly DependencyProperty PopupModeProperty = DependencyProperty.Register(
nameof(PopupMode), typeof(PopupBoxPopupMode), typeof(PopupBox), new PropertyMetadata(default(PopupBoxPopupMode)));

Expand Down
Loading