├── .github └── workflows │ └── nuget.yml ├── .gitignore ├── DropdownMenuControl ├── AssemblyInfo.cs ├── DropdownMenu.cs ├── DropdownMenuControl.csproj ├── DropdownMenuControl.csproj.user └── Themes │ └── Generic.xaml ├── HamburgerMenuControl ├── AssemblyInfo.cs ├── HamburgerMenu.cs ├── HamburgerMenuControl.csproj ├── HamburgerMenuControl.csproj.user ├── HamburgerMenuItem.cs └── Themes │ └── Generic.xaml ├── HighlightTextBlockControl ├── AssemblyInfo.cs ├── HighlightTextBlock.cs ├── HighlightTextBlockControl.csproj ├── HighlightTextBlockControl.csproj.user └── Themes │ └── Generic.xaml ├── HoldSubmitButtonControl ├── AssemblyInfo.cs ├── Converters │ ├── DurationSecondsSubtractionConverter.cs │ └── MultiplyConverter.cs ├── HoldSubmitButton.cs ├── HoldSubmitButtonControl.csproj ├── HoldSubmitButtonControl.csproj.user └── Themes │ └── Generic.xaml ├── LoadingSpinnerControl ├── AssemblyInfo.cs ├── Converters │ └── DiameterAndThicknessToStrokeDashArrayConverter.cs ├── LoadingSpinner.cs ├── LoadingSpinnerControl.csproj ├── LoadingSpinnerControl.csproj.user └── Themes │ └── Generic.xaml ├── ModalControl ├── AssemblyInfo.cs ├── Modal.cs ├── ModalControl.csproj ├── ModalControl.csproj.user └── Themes │ └── Generic.xaml ├── PlaceholderTextBox ├── AssemblyInfo.cs ├── PlaceholderTextBox.csproj ├── PlaceholderTextBox.csproj.user ├── PlaceholderTextBoxControl.cs └── Themes │ └── Generic.xaml ├── UIWorkshops.Demo ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── UIWorkshops.Demos.csproj ├── UIWorkshops.Demos.csproj.user └── Windows │ ├── DropdownMenuDemoWindow.xaml │ ├── DropdownMenuDemoWindow.xaml.cs │ ├── HamburgerMenuDemoWindow.xaml │ ├── HamburgerMenuDemoWindow.xaml.cs │ ├── HighlightTextBlockDemoWindow.xaml │ ├── HighlightTextBlockDemoWindow.xaml.cs │ ├── HoldSubmitButtonDemoWindow.xaml │ ├── HoldSubmitButtonDemoWindow.xaml.cs │ ├── LoadingSpinnerDemoWindow.xaml │ ├── LoadingSpinnerDemoWindow.xaml.cs │ ├── ModalDemoWindow.xaml │ ├── ModalDemoWindow.xaml.cs │ ├── PlaceholderTextBoxDemoWindow.xaml │ └── PlaceholderTextBoxDemoWindow.xaml.cs └── UIWorkshops.sln /.github/workflows/nuget.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy Loading Spinner to NuGet" 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'release/loading-spinner' 7 | 8 | env: 9 | BUILD_CONFIGURATION: Release 10 | PACKAGE_OUTPUT_DIRECTORY: ${{ github.workspace }}\output 11 | NUGET_SOURCE_URL: "https://api.nuget.org/v3/index.json" 12 | PACK_OPTIONS: "--no-restore --no-build --include-symbols --include-source" 13 | 14 | jobs: 15 | deploy: 16 | runs-on: windows-latest 17 | steps: 18 | # Setup 19 | - uses: actions/checkout@v2 20 | 21 | - name: Setup .NET Core 22 | uses: actions/setup-dotnet@v1 23 | with: 24 | dotnet-version: "3.1.x" 25 | 26 | # Fix annoying packages not being found 27 | - name: Clean 28 | run: dotnet clean -c ${{ env.BUILD_CONFIGURATION }} && dotnet nuget locals all --clear 29 | 30 | # .NET 31 | - name: Restore .NET packages 32 | run: dotnet restore 33 | 34 | - name: Build .NET projects 35 | run: dotnet build --no-restore -c ${{ env.BUILD_CONFIGURATION }} 36 | 37 | - name: "Pack Loading Spinner Control project" 38 | run: dotnet pack LoadingSpinnerControl -c ${{ env.BUILD_CONFIGURATION }} --output ${{ env.PACKAGE_OUTPUT_DIRECTORY }} ${{ env.PACK_OPTIONS }} 39 | 40 | - name: "Push packages" 41 | run: dotnet nuget push ${{ env.PACKAGE_OUTPUT_DIRECTORY }}\*.nupkg -k ${{ secrets.NUGET_AUTH_TOKEN }} -s ${{ env.NUGET_SOURCE_URL }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | obj/ 2 | bin/ 3 | .vs/ -------------------------------------------------------------------------------- /DropdownMenuControl/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /DropdownMenuControl/DropdownMenu.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Controls.Primitives; 9 | using System.Windows.Data; 10 | using System.Windows.Documents; 11 | using System.Windows.Input; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Imaging; 14 | using System.Windows.Navigation; 15 | using System.Windows.Shapes; 16 | 17 | namespace DropdownMenuControl 18 | { 19 | [TemplatePart(Name = PART_POPUP_NAME, Type = typeof(Popup))] 20 | [TemplatePart(Name = PART_TOGGLE_NAME, Type = typeof(CheckBox))] 21 | public class DropdownMenu : ContentControl 22 | { 23 | private const string PART_POPUP_NAME = "PART_Popup"; 24 | private const string PART_TOGGLE_NAME = "PART_Toggle"; 25 | 26 | private Popup _popup; 27 | private CheckBox _toggle; 28 | 29 | public static readonly DependencyProperty IsOpenProperty = 30 | DependencyProperty.Register("IsOpen", typeof(bool), typeof(DropdownMenu), 31 | new PropertyMetadata(false)); 32 | 33 | public bool IsOpen 34 | { 35 | get { return (bool)GetValue(IsOpenProperty); } 36 | set { SetValue(IsOpenProperty, value); } 37 | } 38 | 39 | static DropdownMenu() 40 | { 41 | DefaultStyleKeyProperty.OverrideMetadata(typeof(DropdownMenu), new FrameworkPropertyMetadata(typeof(DropdownMenu))); 42 | } 43 | 44 | public override void OnApplyTemplate() 45 | { 46 | _popup = Template.FindName(PART_POPUP_NAME, this) as Popup; 47 | if (_popup != null) 48 | { 49 | _popup.Closed += Popup_Closed; 50 | } 51 | 52 | _toggle = Template.FindName(PART_TOGGLE_NAME, this) as CheckBox; 53 | 54 | base.OnApplyTemplate(); 55 | } 56 | 57 | private void Popup_Closed(object sender, EventArgs e) 58 | { 59 | if (!_toggle.IsMouseOver) 60 | { 61 | IsOpen = false; 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /DropdownMenuControl/DropdownMenuControl.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | 7 | 8 | 9 | DropdownMenu.WPF 10 | 1.0.0 11 | SingletonSean 12 | snupkg 13 | https://github.com/SingletonSean/wpf-ui-workshops 14 | https://github.com/SingletonSean/wpf-ui-workshops 15 | wpf custom dropdown menu item triple dot 16 | 17 | A custom, easy to use dropdown menu control for WPF applications. 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /DropdownMenuControl/DropdownMenuControl.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Designer 7 | 8 | 9 | -------------------------------------------------------------------------------- /DropdownMenuControl/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 | 5 | 39 | 40 | -------------------------------------------------------------------------------- /HamburgerMenuControl/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /HamburgerMenuControl/HamburgerMenu.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Animation; 13 | using System.Windows.Media.Imaging; 14 | using System.Windows.Navigation; 15 | using System.Windows.Shapes; 16 | 17 | namespace HamburgerMenuControl 18 | { 19 | public class HamburgerMenu : Control 20 | { 21 | public static readonly DependencyProperty IsOpenProperty = 22 | DependencyProperty.Register("IsOpen", typeof(bool), typeof(HamburgerMenu), 23 | new PropertyMetadata(false, OnIsOpenPropertyChanged)); 24 | 25 | public bool IsOpen 26 | { 27 | get { return (bool)GetValue(IsOpenProperty); } 28 | set { SetValue(IsOpenProperty, value); } 29 | } 30 | 31 | public static readonly DependencyProperty OpenCloseDurationProperty = 32 | DependencyProperty.Register("OpenCloseDuration", typeof(Duration), typeof(HamburgerMenu), 33 | new PropertyMetadata(Duration.Automatic)); 34 | 35 | public Duration OpenCloseDuration 36 | { 37 | get { return (Duration)GetValue(OpenCloseDurationProperty); } 38 | set { SetValue(OpenCloseDurationProperty, value); } 39 | } 40 | 41 | public static readonly DependencyProperty FallbackOpenWidthProperty = 42 | DependencyProperty.Register("FallbackOpenWidth", typeof(double), typeof(HamburgerMenu), 43 | new PropertyMetadata(100.0)); 44 | 45 | public double FallbackOpenWidth 46 | { 47 | get { return (double)GetValue(FallbackOpenWidthProperty); } 48 | set { SetValue(FallbackOpenWidthProperty, value); } 49 | } 50 | 51 | public static readonly DependencyProperty ContentProperty = 52 | DependencyProperty.Register("Content", typeof(FrameworkElement), typeof(HamburgerMenu), 53 | new PropertyMetadata(null)); 54 | 55 | public FrameworkElement Content 56 | { 57 | get { return (FrameworkElement)GetValue(ContentProperty); } 58 | set { SetValue(ContentProperty, value); } 59 | } 60 | 61 | static HamburgerMenu() 62 | { 63 | DefaultStyleKeyProperty.OverrideMetadata(typeof(HamburgerMenu), new FrameworkPropertyMetadata(typeof(HamburgerMenu))); 64 | } 65 | 66 | public HamburgerMenu() 67 | { 68 | Width = 0; 69 | } 70 | 71 | private static void OnIsOpenPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 72 | { 73 | if(d is HamburgerMenu hamburgerMenu) 74 | { 75 | hamburgerMenu.OnIsOpenPropertyChanged(); 76 | } 77 | } 78 | 79 | private void OnIsOpenPropertyChanged() 80 | { 81 | if(IsOpen) 82 | { 83 | OpenMenuAnimated(); 84 | } 85 | else 86 | { 87 | CloseMenuAnimated(); 88 | } 89 | } 90 | 91 | private void OpenMenuAnimated() 92 | { 93 | double contentWidth = GetDesiredContentWidth(); 94 | 95 | DoubleAnimation openingAnimation = new DoubleAnimation(contentWidth, OpenCloseDuration); 96 | BeginAnimation(WidthProperty, openingAnimation); 97 | } 98 | 99 | private double GetDesiredContentWidth() 100 | { 101 | if(Content == null) 102 | { 103 | return FallbackOpenWidth; 104 | } 105 | 106 | Content.Measure(new Size(MaxWidth, MaxHeight)); 107 | 108 | return Content.DesiredSize.Width; 109 | } 110 | 111 | private void CloseMenuAnimated() 112 | { 113 | DoubleAnimation closingAnimation = new DoubleAnimation(0, OpenCloseDuration); 114 | BeginAnimation(WidthProperty, closingAnimation); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /HamburgerMenuControl/HamburgerMenuControl.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /HamburgerMenuControl/HamburgerMenuControl.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Designer 7 | 8 | 9 | -------------------------------------------------------------------------------- /HamburgerMenuControl/HamburgerMenuItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | 7 | namespace HamburgerMenuControl 8 | { 9 | public class HamburgerMenuItem : RadioButton 10 | { 11 | static HamburgerMenuItem() 12 | { 13 | DefaultStyleKeyProperty.OverrideMetadata(typeof(HamburgerMenuItem), new FrameworkPropertyMetadata(typeof(HamburgerMenuItem))); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /HamburgerMenuControl/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 | 5 | 19 | 20 | 35 | 36 | -------------------------------------------------------------------------------- /HighlightTextBlockControl/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /HighlightTextBlockControl/HighlightTextBlock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace HighlightTextBlockControl 17 | { 18 | [TemplatePart(Name = TEXT_DISPLAY_PART_NAME, Type = typeof(TextBlock))] 19 | public class HighlightTextBlock : Control 20 | { 21 | private const string TEXT_DISPLAY_PART_NAME = "PART_TextDisplay"; 22 | 23 | private TextBlock _displayTextBlock; 24 | 25 | public static readonly DependencyProperty HighlightTextProperty = 26 | DependencyProperty.Register("HighlightText", typeof(string), typeof(HighlightTextBlock), 27 | new PropertyMetadata(string.Empty, OnHighlightTextPropertyChanged)); 28 | 29 | public string HighlightText 30 | { 31 | get { return (string)GetValue(HighlightTextProperty); } 32 | set { SetValue(HighlightTextProperty, value); } 33 | } 34 | 35 | public static readonly DependencyProperty TextProperty = 36 | TextBlock.TextProperty.AddOwner(typeof(HighlightTextBlock), 37 | new PropertyMetadata(string.Empty, OnHighlightTextPropertyChanged)); 38 | 39 | public string Text 40 | { 41 | get { return (string)GetValue(TextProperty); } 42 | set { SetValue(TextProperty, value); } 43 | } 44 | 45 | public static readonly DependencyProperty HighlightRunStyleProperty = 46 | DependencyProperty.Register("HighlightRunStyle", typeof(Style), typeof(HighlightTextBlock), 47 | new PropertyMetadata(CreateDefaultHighlightRunStyle())); 48 | 49 | private static object CreateDefaultHighlightRunStyle() 50 | { 51 | Style style = new Style(typeof(Run)); 52 | style.Setters.Add(new Setter(Run.BackgroundProperty, Brushes.Yellow)); 53 | 54 | return style; 55 | } 56 | 57 | public Style HighlightRunStyle 58 | { 59 | get { return (Style)GetValue(HighlightRunStyleProperty); } 60 | set { SetValue(HighlightRunStyleProperty, value); } 61 | } 62 | 63 | static HighlightTextBlock() 64 | { 65 | DefaultStyleKeyProperty.OverrideMetadata(typeof(HighlightTextBlock), new FrameworkPropertyMetadata(typeof(HighlightTextBlock))); 66 | } 67 | 68 | public override void OnApplyTemplate() 69 | { 70 | _displayTextBlock = Template.FindName(TEXT_DISPLAY_PART_NAME, this) as TextBlock; 71 | UpdateHighlightDisplay(); 72 | 73 | base.OnApplyTemplate(); 74 | } 75 | 76 | private static void OnHighlightTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 77 | { 78 | if(d is HighlightTextBlock highlightTextBlock) 79 | { 80 | highlightTextBlock.UpdateHighlightDisplay(); 81 | } 82 | } 83 | 84 | private void UpdateHighlightDisplay() 85 | { 86 | if(_displayTextBlock != null) 87 | { 88 | _displayTextBlock.Inlines.Clear(); 89 | 90 | int highlightTextLength = HighlightText.Length; 91 | if(highlightTextLength == 0) 92 | { 93 | _displayTextBlock.Text = Text; 94 | } 95 | else 96 | { 97 | for (int i = 0; i < Text.Length; i++) 98 | { 99 | if(i + highlightTextLength > Text.Length) 100 | { 101 | _displayTextBlock.Inlines.Add(new Run(Text.Substring(i))); 102 | break; 103 | } 104 | 105 | int nextHighlightTextIndex = Text.IndexOf(HighlightText, i); 106 | if(nextHighlightTextIndex == -1) 107 | { 108 | _displayTextBlock.Inlines.Add(new Run(Text.Substring(i))); 109 | break; 110 | } 111 | 112 | _displayTextBlock.Inlines.Add(new Run(Text.Substring(i, nextHighlightTextIndex - i))); 113 | _displayTextBlock.Inlines.Add(CreateHighlightedRun(HighlightText)); 114 | 115 | i = nextHighlightTextIndex + highlightTextLength - 1; 116 | } 117 | } 118 | } 119 | } 120 | 121 | private Run CreateHighlightedRun(string text) 122 | { 123 | return new Run(text) 124 | { 125 | Style = HighlightRunStyle 126 | }; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /HighlightTextBlockControl/HighlightTextBlockControl.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /HighlightTextBlockControl/HighlightTextBlockControl.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Designer 7 | 8 | 9 | -------------------------------------------------------------------------------- /HighlightTextBlockControl/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 | 5 | 14 | 15 | -------------------------------------------------------------------------------- /HoldSubmitButtonControl/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /HoldSubmitButtonControl/Converters/DurationSecondsSubtractionConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Text; 5 | using System.Windows; 6 | using System.Windows.Data; 7 | 8 | namespace HoldSubmitButtonControl.Converters 9 | { 10 | public class DurationSecondsSubtractionConverter : IValueConverter 11 | { 12 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 13 | { 14 | if(value is Duration duration && double.TryParse(parameter.ToString(), out double seconds)) 15 | { 16 | return duration.Subtract(TimeSpan.FromSeconds(seconds)); 17 | } 18 | 19 | return value; 20 | } 21 | 22 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 23 | { 24 | throw new NotImplementedException(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /HoldSubmitButtonControl/Converters/MultiplyConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Text; 5 | using System.Windows.Data; 6 | 7 | namespace HoldSubmitButtonControl.Converters 8 | { 9 | public class MultiplyConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | return double.TryParse(value.ToString(), out double num1) && 14 | double.TryParse(parameter.ToString(), out double num2) ? 15 | num1 * num2 : 0; 16 | } 17 | 18 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 19 | { 20 | throw new NotImplementedException(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /HoldSubmitButtonControl/HoldSubmitButton.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | using System.Windows.Documents; 11 | using System.Windows.Input; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Imaging; 14 | using System.Windows.Navigation; 15 | using System.Windows.Shapes; 16 | 17 | namespace HoldSubmitButtonControl 18 | { 19 | public class HoldSubmitButton : Button 20 | { 21 | private CancellationTokenSource _cancellationTokenSource; 22 | 23 | public static readonly DependencyProperty HoldDurationProperty = 24 | DependencyProperty.Register("HoldDuration", typeof(Duration), typeof(HoldSubmitButton), 25 | new PropertyMetadata(Duration.Automatic, null, CoerceHoldDuration)); 26 | 27 | private static object CoerceHoldDuration(DependencyObject d, object baseValue) 28 | { 29 | if(baseValue is Duration duration && duration.TimeSpan.TotalSeconds >= 0.5) 30 | { 31 | return baseValue; 32 | } 33 | 34 | return new Duration(TimeSpan.FromSeconds(0.5)); 35 | } 36 | 37 | public Duration HoldDuration 38 | { 39 | get { return (Duration)GetValue(HoldDurationProperty); } 40 | set { SetValue(HoldDurationProperty, value); } 41 | } 42 | 43 | public static readonly RoutedEvent HoldCompletedEvent = 44 | EventManager.RegisterRoutedEvent(nameof(HoldCompleted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(HoldSubmitButton)); 45 | 46 | public event RoutedEventHandler HoldCompleted 47 | { 48 | add => AddHandler(HoldCompletedEvent, value); 49 | remove => RemoveHandler(HoldCompletedEvent, value); 50 | } 51 | 52 | public static readonly RoutedEvent HoldCancelledEvent = 53 | EventManager.RegisterRoutedEvent(nameof(HoldCancelled), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(HoldSubmitButton)); 54 | 55 | public event RoutedEventHandler HoldCancelled 56 | { 57 | add => AddHandler(HoldCancelledEvent, value); 58 | remove => RemoveHandler(HoldCancelledEvent, value); 59 | } 60 | 61 | static HoldSubmitButton() 62 | { 63 | DefaultStyleKeyProperty.OverrideMetadata(typeof(HoldSubmitButton), new FrameworkPropertyMetadata(typeof(HoldSubmitButton))); 64 | } 65 | 66 | protected override async void OnMouseLeftButtonDown(MouseButtonEventArgs e) 67 | { 68 | _cancellationTokenSource = new CancellationTokenSource(); 69 | 70 | try 71 | { 72 | await Task.Delay((int)HoldDuration.TimeSpan.TotalMilliseconds, _cancellationTokenSource.Token); 73 | base.OnClick(); 74 | 75 | RaiseEvent(new RoutedEventArgs(HoldCompletedEvent)); 76 | } 77 | catch (TaskCanceledException) 78 | { 79 | RaiseEvent(new RoutedEventArgs(HoldCancelledEvent)); 80 | } 81 | 82 | base.OnMouseLeftButtonDown(e); 83 | } 84 | 85 | protected override void OnClick() { } 86 | 87 | protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) 88 | { 89 | CancelSubmit(); 90 | base.OnMouseLeftButtonUp(e); 91 | } 92 | 93 | protected override void OnMouseLeave(MouseEventArgs e) 94 | { 95 | CancelSubmit(); 96 | base.OnMouseLeave(e); 97 | } 98 | 99 | private void CancelSubmit() 100 | { 101 | if(_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested) 102 | { 103 | _cancellationTokenSource.Cancel(); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /HoldSubmitButtonControl/HoldSubmitButtonControl.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /HoldSubmitButtonControl/HoldSubmitButtonControl.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Designer 7 | 8 | 9 | -------------------------------------------------------------------------------- /HoldSubmitButtonControl/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 | 6 | 125 | 126 | -------------------------------------------------------------------------------- /LoadingSpinnerControl/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /LoadingSpinnerControl/Converters/DiameterAndThicknessToStrokeDashArrayConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Text; 5 | using System.Windows.Data; 6 | using System.Windows.Media; 7 | 8 | namespace LoadingSpinnerControl.Converters 9 | { 10 | public class DiameterAndThicknessToStrokeDashArrayConverter : IMultiValueConverter 11 | { 12 | public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) 13 | { 14 | if(values.Length < 2 || 15 | !double.TryParse(values[0].ToString(), out double diameter) || 16 | !double.TryParse(values[1].ToString(), out double thickness)) 17 | { 18 | return new DoubleCollection(new[] { 0.0 }); 19 | } 20 | 21 | double circumference = Math.PI * diameter; 22 | 23 | double lineLength = circumference * 0.75; 24 | double gapLength = circumference - lineLength; 25 | 26 | return new DoubleCollection(new[] { lineLength / thickness, gapLength / thickness }); 27 | } 28 | 29 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LoadingSpinnerControl/LoadingSpinner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace LoadingSpinnerControl 17 | { 18 | public class LoadingSpinner : Control 19 | { 20 | public static readonly DependencyProperty IsLoadingProperty = 21 | DependencyProperty.Register("IsLoading", typeof(bool), typeof(LoadingSpinner), 22 | new PropertyMetadata(false)); 23 | 24 | public bool IsLoading 25 | { 26 | get { return (bool)GetValue(IsLoadingProperty); } 27 | set { SetValue(IsLoadingProperty, value); } 28 | } 29 | 30 | public static readonly DependencyProperty DiameterProperty = 31 | DependencyProperty.Register("Diameter", typeof(double), typeof(LoadingSpinner), 32 | new PropertyMetadata(100.0)); 33 | 34 | public double Diameter 35 | { 36 | get { return (double)GetValue(DiameterProperty); } 37 | set { SetValue(DiameterProperty, value); } 38 | } 39 | 40 | public static readonly DependencyProperty ThicknessProperty = 41 | DependencyProperty.Register("Thickness", typeof(double), typeof(LoadingSpinner), 42 | new PropertyMetadata(1.0)); 43 | 44 | public double Thickness 45 | { 46 | get { return (double)GetValue(ThicknessProperty); } 47 | set { SetValue(ThicknessProperty, value); } 48 | } 49 | 50 | public static readonly DependencyProperty ColorProperty = 51 | DependencyProperty.Register("Color", typeof(Brush), typeof(LoadingSpinner), 52 | new PropertyMetadata(Brushes.Black)); 53 | 54 | public Brush Color 55 | { 56 | get { return (Brush)GetValue(ColorProperty); } 57 | set { SetValue(ColorProperty, value); } 58 | } 59 | 60 | public static readonly DependencyProperty CapProperty = 61 | DependencyProperty.Register("Cap", typeof(PenLineCap), typeof(LoadingSpinner), 62 | new PropertyMetadata(PenLineCap.Flat)); 63 | 64 | public PenLineCap Cap 65 | { 66 | get { return (PenLineCap)GetValue(CapProperty); } 67 | set { SetValue(CapProperty, value); } 68 | } 69 | 70 | static LoadingSpinner() 71 | { 72 | DefaultStyleKeyProperty.OverrideMetadata(typeof(LoadingSpinner), new FrameworkPropertyMetadata(typeof(LoadingSpinner))); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /LoadingSpinnerControl/LoadingSpinnerControl.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | 7 | 8 | 9 | LoadingSpinner.WPF 10 | 1.0.0 11 | SingletonSean 12 | snupkg 13 | https://github.com/SingletonSean/wpf-ui-workshops 14 | https://github.com/SingletonSean/wpf-ui-workshops 15 | wpf custom loading load spinner control 16 | 17 | A custom, easy to use loading spinner control for WPF applications. 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LoadingSpinnerControl/LoadingSpinnerControl.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Designer 7 | 8 | 9 | -------------------------------------------------------------------------------- /LoadingSpinnerControl/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 | 6 | 50 | 51 | -------------------------------------------------------------------------------- /ModalControl/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /ModalControl/Modal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace ModalControl 17 | { 18 | public class Modal : ContentControl 19 | { 20 | public static readonly DependencyProperty IsOpenProperty = 21 | DependencyProperty.Register("IsOpen", typeof(bool), typeof(Modal), 22 | new PropertyMetadata(false)); 23 | 24 | public bool IsOpen 25 | { 26 | get { return (bool)GetValue(IsOpenProperty); } 27 | set { SetValue(IsOpenProperty, value); } 28 | } 29 | 30 | static Modal() 31 | { 32 | DefaultStyleKeyProperty.OverrideMetadata(typeof(Modal), new FrameworkPropertyMetadata(typeof(Modal))); 33 | BackgroundProperty.OverrideMetadata(typeof(Modal), new FrameworkPropertyMetadata(CreateDefaultBackground())); 34 | } 35 | 36 | private static object CreateDefaultBackground() 37 | { 38 | return new SolidColorBrush(Colors.Black) 39 | { 40 | Opacity = 0.3 41 | }; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ModalControl/ModalControl.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | 7 | 8 | 9 | SimpleModal.WPF 10 | 1.0.0 11 | SingletonSean 12 | snupkg 13 | https://github.com/SingletonSean/wpf-ui-workshops 14 | https://github.com/SingletonSean/wpf-ui-workshops 15 | wpf custom modal popup control 16 | 17 | A custom, easy to use modal control for WPF applications. 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ModalControl/ModalControl.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Designer 7 | 8 | 9 | -------------------------------------------------------------------------------- /ModalControl/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 | 5 | 41 | 42 | -------------------------------------------------------------------------------- /PlaceholderTextBox/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /PlaceholderTextBox/PlaceholderTextBox.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PlaceholderTextBox/PlaceholderTextBox.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Designer 7 | 8 | 9 | -------------------------------------------------------------------------------- /PlaceholderTextBox/PlaceholderTextBoxControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace PlaceholderTextBox 17 | { 18 | public class PlaceholderTextBoxControl : TextBox 19 | { 20 | public static readonly DependencyProperty IsEmptyProperty = 21 | DependencyProperty.Register("IsEmpty", typeof(bool), typeof(PlaceholderTextBoxControl), 22 | new PropertyMetadata(false)); 23 | 24 | public bool IsEmpty 25 | { 26 | get { return (bool)GetValue(IsEmptyProperty); } 27 | private set { SetValue(IsEmptyProperty, value); } 28 | } 29 | 30 | static PlaceholderTextBoxControl() 31 | { 32 | DefaultStyleKeyProperty.OverrideMetadata(typeof(PlaceholderTextBoxControl), new FrameworkPropertyMetadata(typeof(PlaceholderTextBoxControl))); 33 | } 34 | 35 | protected override void OnInitialized(EventArgs e) 36 | { 37 | UpdateIsEmpty(); 38 | base.OnInitialized(e); 39 | } 40 | 41 | protected override void OnTextChanged(TextChangedEventArgs e) 42 | { 43 | UpdateIsEmpty(); 44 | base.OnTextChanged(e); 45 | } 46 | 47 | private void UpdateIsEmpty() 48 | { 49 | IsEmpty = string.IsNullOrEmpty(Text); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /PlaceholderTextBox/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 | 5 | 33 | 34 | -------------------------------------------------------------------------------- /UIWorkshops.Demo/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | -------------------------------------------------------------------------------- /UIWorkshops.Demo/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using UIWorkshops.Demos.Windows; 3 | 4 | namespace UIWorkshops.Demos 5 | { 6 | public partial class App : Application 7 | { 8 | protected override void OnStartup(StartupEventArgs e) 9 | { 10 | MainWindow = new DropdownMenuDemoWindow(); 11 | MainWindow.Show(); 12 | 13 | base.OnStartup(e); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /UIWorkshops.Demo/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /UIWorkshops.Demo/UIWorkshops.Demos.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | netcoreapp3.1 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /UIWorkshops.Demo/UIWorkshops.Demos.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Designer 7 | 8 | 9 | 10 | 11 | Code 12 | 13 | 14 | Code 15 | 16 | 17 | Code 18 | 19 | 20 | 21 | 22 | Designer 23 | 24 | 25 | Designer 26 | 27 | 28 | Designer 29 | 30 | 31 | Designer 32 | 33 | 34 | Designer 35 | 36 | 37 | Designer 38 | 39 | 40 | Designer 41 | 42 | 43 | -------------------------------------------------------------------------------- /UIWorkshops.Demo/Windows/DropdownMenuDemoWindow.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 89 | 90 | 94 | 95 |