Extending TextBox in WPF using MahApps keeping Style - c#

I made a custom textbox class for validating the input of the user to only allow Hexadecimal values, and used this new textbox (HexTextBox) in the xaml. It works well, but the HexTextBox looses all the style from the Mahapps, including color scheme and TextBoxHelper. Do you know how to use this extended TexBox and keep the style?
HexTextBox:
public class HexTextBox : TextBox
{
public HexTextBox()
{
}
/// <summary>
/// Raise when a keyboard key is pressed.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Space)
{
e.Handled = true;
}
base.OnPreviewKeyDown(e);
}
/// <summary>
/// Raise when a text will be inputed in the text box object.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnTextInput(TextCompositionEventArgs e)
{
int hexNumber;
e.Handled = !int.TryParse(e.Text, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out hexNumber);
base.OnTextInput(e);
}
}
Window.xaml
<UserControl
...
xmlns:CoreWPF="clr-namespace:CoreWPF;assembly=CoreWPF"
...>
<CoreWPF:HexTextBox
Text="{Binding DataXor1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Grid.Column="2" Grid.Row="0"
controls:TextBoxHelper.ClearTextButton="True"
Height="26"
TextWrapping="Wrap"
CharacterCasing="Upper"
VerticalAlignment="Center"/>
Thanks in advance!

Create default style for your custom control which will be based on TextBox style.
<Style TargetType="Controls:HexTextBox" BasedOn="{StaticResource {x:Type TextBox}}"/>

Related

How to handle Header click event in DataGrid in the context of MVVM?

I know how to handle Header click event in DataGrid WPF using code-behind approach:
<DataGrid>
<DataGrid.Resources>
<Style TargetType="DataGridColumnHeader">
<EventSetter Event="Click" Handler="columnHeader_Click" />
</Style>
</DataGrid.Resources>
</DataGrid>
However, I would like to handle this event in the context of MVVM. I've tried to handle by System.Windows.Interactivity, but I cannot figure out what I should write inside of EventName:
<DataGrid>
<i:Interaction.Triggers>
<i:EventTrigger EventName="What event can I use?"
</i:Interaction.Triggers>
</DataGrid>
I've seen events such as ColumnReordered, ColumnReordering, but it is not applicable.
How can I handle Header click event in DataGrid WPF in the context of MVVM?
You need to set the trigger in the header style. Interaction triggers and behaviours can't ordinarily be set in a style because the TriggerCollection and BehaviorCollection classes are sealed, so you can't declare them in XAML. However, user vspivak posted a workaround for this on the wordpress site which basically creates its own lists and keeps them synchronized with those in the Interaction lists:
using System.Collections.Generic;
using System.Windows;
using System.Windows.Interactivity;
namespace YourApplication
{
/// <summary>
/// <see cref="FrameworkTemplate"/> for InteractivityElements instance
/// <remarks>Subclassed for forward compatibility, perhaps one day <see cref="FrameworkTemplate"/> </remarks>
/// <remarks>will not be partially internal</remarks>
/// </summary>
public class InteractivityTemplate : DataTemplate
{
}
/// <summary>
/// Holder for interactivity entries
/// </summary>
public class InteractivityItems : FrameworkElement
{
private List<Behavior> _behaviors;
private List<System.Windows.Interactivity.TriggerBase> _triggers;
/// <summary>
/// Storage for triggers
/// </summary>
public List<System.Windows.Interactivity.TriggerBase> Triggers
{
get
{
if (_triggers == null)
_triggers = new List<System.Windows.Interactivity.TriggerBase>();
return _triggers;
}
}
/// <summary>
/// Storage for Behaviors
/// </summary>
public List<Behavior> Behaviors
{
get
{
if (_behaviors == null)
_behaviors = new List<Behavior>();
return _behaviors;
}
}
#region Template attached property
public static InteractivityTemplate GetTemplate(DependencyObject obj)
{
return (InteractivityTemplate)obj.GetValue(TemplateProperty);
}
public static void SetTemplate(DependencyObject obj, InteractivityTemplate value)
{
obj.SetValue(TemplateProperty, value);
}
public static readonly DependencyProperty TemplateProperty =
DependencyProperty.RegisterAttached("Template",
typeof(InteractivityTemplate),
typeof(InteractivityItems),
new PropertyMetadata(default(InteractivityTemplate), OnTemplateChanged));
private static void OnTemplateChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
InteractivityTemplate dt = (InteractivityTemplate)e.NewValue;
#if(!SILVERLIGHT)
dt.Seal();
#endif
InteractivityItems ih = (InteractivityItems)dt.LoadContent();
BehaviorCollection bc = Interaction.GetBehaviors(d);
System.Windows.Interactivity.TriggerCollection tc = Interaction.GetTriggers(d);
foreach (Behavior behavior in ih.Behaviors)
bc.Add(behavior);
foreach (System.Windows.Interactivity.TriggerBase trigger in ih.Triggers)
tc.Add(trigger);
}
#endregion
}
}
With this in place it's a simple matter of applying it to the ColumnHeaderStyle:
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="local:InteractivityItems.Template">
<Setter.Value>
<local:InteractivityTemplate>
<local:InteractivityItems>
<local:InteractivityItems.Triggers>
<i:EventTrigger EventName="Click" >
<i:InvokeCommandAction Command="{Binding DataContext.YourCommand,
RelativeSource={RelativeSource AncestorType=DataGrid}}" />
</i:EventTrigger>
</local:InteractivityItems.Triggers>
</local:InteractivityItems>
</local:InteractivityTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.ColumnHeaderStyle>
Bit kludgy, but it works.

Update TextBlock with binding not working

I have a question about databinding!
I am writing code for a 'node editor' that has some (different) nodes in it.
I use a BaseViewModel class that derives from INotifyPropertyChanged.
There is a 'base' NodeViewModel (that derives from it) with an ObservableCollection and other Properties, like the Node's Name property. It's implementation looks like this:
(in public class NodeViewModel : BaseViewModel):
protected String mName = String.Empty;
public String Name {
get { return mName; }
set {
if (mName == value) {
return;
}
mName = value;
OnPropertyChanged("Name");
}
}
With an OnPropertyChanged handler that looks like this:
(in BaseViewModel)
protected virtual void OnPropertyChanged(string propertyName) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Now I have one additional RoomViewModel that derives from NodeViewModel.
I use another different ViewModel that I call RoomCollectionViewModel to group some rooms.
Now when I add a room to my roomcollection (by drawing a connection between them) I test all connected rooms for the same name.
If an already connected room exists in the collection with the same room name (e.g. "new room") I want to change those two room's names to e.g. "new room #1" and "new room #2". No problem so far.
Every node control (created using DataTemplates with set DataContext set to the ViewModel) contains a TextBlock (a modified one) that displays the node's name.
This is where it gets problematic:
I use a modified Textblock because I want to be able to modify the node's name by double-clicking on it. And that works perfectly, only if I modify the RoomViewModel's name in Code, this (modified) TextBlock won't update.
The strange thing is this:
When two equally named rooms in a collection get renamed by my code and I then double-click on the editable TextBlock (which converts to a TextBox in that process), I already see the modified Text. So I assume my DataBinding and my code is correct, just not complete :)
So how is it possible to force an update of my EditableTextBlock, the Text (DependencyProperty) seems to be updated correctly...
I hope you understand what my problem is! Thank you for any help.
Update 1
This is the XAML code for my EditableTextBlock (it comes from here: http://www.codeproject.com/Articles/31592/Editable-TextBlock-in-WPF-for-In-place-Editing)
<UserControl x:Class="NetworkUI.EditableTextBlock"
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:NetworkUI"
mc:Ignorable="d"
d:DesignHeight="60" d:DesignWidth="240" x:Name="mainControl">
<UserControl.Resources>
<DataTemplate x:Key="EditModeTemplate">
<TextBox KeyDown="TextBox_KeyDown" Loaded="TextBox_Loaded" LostFocus="TextBox_LostFocus"
Text="{Binding ElementName=mainControl, Path=Text, UpdateSourceTrigger=PropertyChanged}"
Margin="0" BorderThickness="1" />
</DataTemplate>
<DataTemplate x:Key="DisplayModeTemplate">
<TextBlock Text="{Binding ElementName=mainControl, Path=FormattedText}" Margin="5,3,5,3" MouseDown="TextBlock_MouseDown" />
</DataTemplate>
<Style TargetType="{x:Type local:EditableTextBlock}">
<Style.Triggers>
<Trigger Property="IsInEditMode" Value="True">
<Setter Property="ContentTemplate" Value="{StaticResource EditModeTemplate}" />
</Trigger>
<Trigger Property="IsInEditMode" Value="False">
<Setter Property="ContentTemplate" Value="{StaticResource DisplayModeTemplate}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
And here is the code-behind file:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace NetworkUI {
/// <summary>
/// Interaction logic for EditableTextBlock.xaml
/// </summary>
public partial class EditableTextBlock : UserControl {
#region Dependency Properties, Events
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(String), typeof(EditableTextBlock),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty IsEditableProperty =
DependencyProperty.Register("IsEditable", typeof(Boolean), typeof(EditableTextBlock), new PropertyMetadata(true));
public static readonly DependencyProperty IsInEditModeProperty =
DependencyProperty.Register("IsInEditMode", typeof(Boolean), typeof(EditableTextBlock), new PropertyMetadata(false));
public static readonly DependencyProperty TextFormatProperty =
DependencyProperty.Register("TextFormat", typeof(String), typeof(EditableTextBlock), new PropertyMetadata("{0}"));
#endregion ///Dependency Properties, Events
#region Variables and Properties
/// <summary>
/// We keep the old text when we go into editmode
/// in case the user aborts with the escape key
/// </summary>
private String oldText;
/// <summary>
/// Text content of this EditableTextBlock
/// </summary>
public String Text {
get { return (String)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
/// <summary>
/// Is this EditableTextBlock editable or not
/// </summary>
public Boolean IsEditable {
get { return (Boolean)GetValue(IsEditableProperty); }
set { SetValue(IsEditableProperty, value); }
}
/// <summary>
/// Is this EditableTextBlock currently in edit mode
/// </summary>
public Boolean IsInEditMode {
get {
if (IsEditable)
return (Boolean)GetValue(IsInEditModeProperty);
else
return false;
}
set {
if (IsEditable) {
if (value)
oldText = Text;
SetValue(IsInEditModeProperty, value);
}
}
}
/// <summary>
/// The text format for the TextBlock
/// </summary>
public String TextFormat {
get { return (String)GetValue(TextFormatProperty); }
set {
if (value == "")
value = "{0}";
SetValue(TextFormatProperty, value);
}
}
/// <summary>
/// The formatted text of this EditablTextBlock
/// </summary>
public String FormattedText {
get { return String.Format(TextFormat, Text); }
}
#endregion ///Variables and Properties
#region Constructor
/// <summary>
/// Default constructor for the editable text block
/// </summary>
public EditableTextBlock() {
InitializeComponent();
Focusable = true;
FocusVisualStyle = null;
}
#endregion ///Constructor
#region Methods, Functions and Eventhandler
/// <summary>
/// Invoked when we enter edit mode
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event arguments</param>
void TextBox_Loaded(object sender, RoutedEventArgs e) {
TextBox txt = sender as TextBox;
/// Give the TextBox input focus
txt.Focus();
txt.SelectAll();
}
/// <summary>
/// Invoked when we exit edit mode
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event arguments</param>
void TextBox_LostFocus(object sender, RoutedEventArgs e) {
IsInEditMode = false;
}
/// <summary>
/// Invoked when the user edits the annotation.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event arguments</param>
void TextBox_KeyDown(object sender, KeyEventArgs e) {
if (e.Key == Key.Enter) {
IsInEditMode = false;
e.Handled = true;
}
else if (e.Key == Key.Escape) {
IsInEditMode = false;
Text = oldText;
e.Handled = true;
}
}
/// <summary>
/// Invoked when the user double-clicks on the textblock
/// to edit the text
/// </summary>
/// <param name="sender">Sender (the Textblock)</param>
/// <param name="e">Event arguments</param>
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e) {
if (e.ClickCount == 2)
IsInEditMode = true;
}
#endregion ///Methods, Functions and Eventhandler
}
Thank you for any help!
Update 2
I changed the following line of code:
<TextBlock Text="{Binding ElementName=mainControl, Path=FormattedText}" Margin="5,3,5,3" MouseDown="TextBlock_MouseDown" />
to:
<TextBlock Text="{Binding ElementName=mainControl, Path=Text}" Margin="5,3,5,3" MouseDown="TextBlock_MouseDown" />
and now it is working!
I didn't see the TextBlock using the FormattedText in the first place! Ugh, thank you very much, now everything updates perfectly!
As postes by Lee O. the problem was indeed the bound property from my EditableTextBlock control.
The TextBlock used the FormattedText property that was updated by my Binding. Now I use the Text property for both, the TextBlock and the TextBox controls.
I simply removed the FormattedText property as well as the TextFormatProperty (DependencyProperty) and TextFormat property from my EditableTextBlock because I didn't plan to use those.
Thank you again!

Attached properties/events

I have a question regarding custom attached properties/events. In my scenario I want to attach a property/event to any control. The value of this property/event should be an event handler. In short, it should look like:
<TextBox local:Dragging.OnDrag="OnDrag" />
First I tried to implement OnDrag as an attached property. This works for the case above, but then the following case fails:
<Style TargetType="TextBox">
<Setter Property="local:Dragging.OnDrag" Value="OnDrag" />
</Style>
Because the "OnDrag" string can apparently not be made into a RoutedEventHandler (the attached property's type) by the XAML system.
The next thing I tried then was to try and use an attached event, very much like the builtin Mouse.MouseEnter for example.
The complete code for this is shown at the bottom. There are curious things happening with this version:
If you run the code as shown (with the RegisterRoutedEvent line commented) it will show the "Add handler" function is called. Then the xaml system has an internal exception when applying the style (due to missing registered event I guess).
If you run the code with the RegisterRoutedEvent line in effect everything runs, but the "Add handler" function is never called. I want it to be called though, so that I can register at the drag and drop manager.
Curiously, if I change the event in the EventSetter from my own to Mouse.MouseEnter the code that's automatically generated by the xaml designer (in MainWindow.g[.i].cs) is different.
I am not sure why 2) does not call the AddXYZHandler. MSDN seems to indicate this should work.
Finally my questions:
How can I make this work? Is it possible at all?
Do I better use an attached event or an attached property for my scenario?
in case of properties: How do I fix the Style Setter so it converts the OnDrag string to a proper RoutedEventHandler?
in case of events: What's going wrong here? Any way to fix this? I want AddXYZHandler to be called, but apparently that does not work with the style.
MainWindow.xaml:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GridTest.MainWindow"
xmlns:local="clr-namespace:GridTest"
Title="MainWindow" Height="350" Width="525"
local:XYZTest.XYZ="OnXYZAttached">
<Window.Style>
<Style TargetType="Window">
<EventSetter Event="local:XYZTest.XYZ" Handler="OnXYZStyle" />
</Style>
</Window.Style>
</Window>
MainWindow.xaml.cs:
using System.Windows;
namespace GridTest
{
public class XYZTest
{
//public static readonly RoutedEvent XYZEvent = EventManager.RegisterRoutedEvent("XYZ", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(XYZTest));
public static void AddXYZHandler(DependencyObject element, RoutedEventHandler handler)
{
MessageBox.Show("add handler");
}
public static void RemoveXYZHandler(DependencyObject element, RoutedEventHandler handler)
{
MessageBox.Show("remove handler");
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public void OnXYZAttached(object sender, RoutedEventArgs e)
{
MessageBox.Show("attached");
}
public void OnXYZStyle(object sender, RoutedEventArgs e)
{
MessageBox.Show("style");
}
}
}
}
New code:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GridTest.MainWindow"
x:Name="root"
xmlns:local="clr-namespace:GridTest"
local:XYZTest.ABC="OnXYZTopLevel"
Title="MainWindow" Height="350" Width="525">
<ListBox ItemsSource="{Binding}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Background" Value="Red" />
<Setter Property="local:XYZTest.ABC" Value="OnXYZStyle" />
<!-- <Setter Property="local:XYZTest.ABC" Value="{Binding OnXYZStyleProperty, ElementName=root}" /> -->
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Window>
using System.Windows;
namespace GridTest
{
public class XYZTest
{
public static readonly DependencyProperty ABCProperty = DependencyProperty.RegisterAttached("ABC", typeof(RoutedEventHandler), typeof(XYZTest), new UIPropertyMetadata(null, OnABCChanged));
public static void SetABC(UIElement element, RoutedEventHandler value)
{
System.Diagnostics.Debug.WriteLine("ABC set to " + value.Method.Name);
}
static void OnABCChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("ABC changed to " + ((RoutedEventHandler)e.NewValue).Method.Name);
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new[] { "A", "B", "C" };
}
public void OnXYZTopLevel(object sender, RoutedEventArgs e)
{
MessageBox.Show("handler top level");
}
public void OnXYZStyle(object sender, RoutedEventArgs e)
{
MessageBox.Show("handler style");
}
public RoutedEventHandler OnXYZStyleProperty
{
get { return OnXYZStyle; }
}
}
}
I successfully implemented drag and drop functionality completely using Attached Properties. If I were you, I'd avoid using custom events for this, as you're stuck with their parameters. Personally, I went for ICommand instead, but you could also use delegates.
Please look below at the list of properties and Commands that I used in my drag and drop base class implementation:
/// <summary>
/// Gets or sets the type of the drag and drop object required by the Control that the property is set on.
/// </summary>
public Type DragDropType { get; set; }
/// <summary>
/// Gets or sets the allowable types of objects that can be used in drag and drop operations.
/// </summary>
public List<Type> DragDropTypes { get; set; }
/// <summary>
/// Gets or sets the ICommand instance that will be executed when the user attempts to drop a dragged item onto a valid drop target Control.
/// </summary>
public ICommand DropCommand { get; set; }
/// <summary>
/// Gets or sets the DragDropEffects object that specifies the type of the drag and drop operations allowable on the Control that the property is set on.
/// </summary>
public DragDropEffects DragDropEffects { get; set; }
/// <summary>
/// The Point struct that represents the position on screen that the user initiated the drag and drop procedure.
/// </summary>
protected Point DragStartPosition
{
get { return dragStartPosition; }
set { if (dragStartPosition != value) { dragStartPosition = value; } }
}
/// <summary>
/// The UIElement object that represents the UI element that has the attached Adorner control... usually the top level view.
/// </summary>
protected UIElement AdornedUIElement
{
get { return adornedUIElement; }
set { if (adornedUIElement != value) { adornedUIElement = value; } }
}
The AdornedUIElement property holds an Adorner that displays the dragged items as they are dragged, but is optional for you to implement. In this base class, I have implemented most of the drag and drop functionality and exposed protected abstract methods that derived classes must implement. As an example, this method calls the OnAdornedUIElementPreviewDragOver method to provide derived classes an opportunity to change the behaviour of the base class:
private void AdornedUIElementPreviewDragOver(object sender, DragEventArgs e)
{
PositionAdorner(e.GetPosition(adornedUIElement));
OnAdornedUIElementPreviewDragOver(sender, e); // Call derived classes here <<<
if (e.Handled) return; // to bypass base class behaviour
HitTestResult hitTestResult = VisualTreeHelper.HitTest(adornedUIElement, e.GetPosition(adornedUIElement));
Control controlUnderMouse = hitTestResult.VisualHit.GetParentOfType<Control>();
UpdateDragDropEffects(controlUnderMouse, e);
e.Handled = true;
}
/// <summary>
/// Must be overidden in derived classes to call both the UpdateDropProperties and UpdateDragDropEffects methods to provide feedback for the current drag and drop operation.
/// </summary>
/// <param name="sender">The Control that the user dragged the mouse pointer over.</param>
/// <param name="e">The DragEventArgs object that contains arguments relevant to all drag and drop events.</param>
protected abstract void OnAdornedUIElementPreviewDragOver(object sender, DragEventArgs e);
Then in my extended ListBoxDragDropManager class:
protected override void OnAdornedUIElementPreviewDragOver(object sender, DragEventArgs e)
{
HitTestResult hitTestResult = VisualTreeHelper.HitTest(AdornedUIElement, e.GetPosition(AdornedUIElement));
ListBox listBoxUnderMouse = hitTestResult.VisualHit.GetParentOfType<ListBox>();
if (listBoxUnderMouse != null && listBoxUnderMouse.AllowDrop)
{
UpdateDropProperties(ListBoxProperties.GetDragDropType(listBoxUnderMouse), ListBoxProperties.GetDropCommand(listBoxUnderMouse));
}
UpdateDragDropEffects(listBoxUnderMouse, e);
e.Handled = true; // This bypasses base class behaviour
}
Finally, it is used simply in the UI like so (the RelativeSource declarations and narrow width here make it seem worse than it is):
<ListBox ItemsSource="{Binding Disc.Tracks, IsAsync=True}" SelectedItem="{Binding
Disc.Tracks.CurrentItem}" AllowDrop="True" Attached:ListBoxProperties.
IsDragTarget="True" Attached:ListBoxProperties.DropCommand="{Binding
DataContext.DropTracks, RelativeSource={RelativeSource AncestorType={x:Type
Views:ReleaseTracksView}}}" Attached:ListBoxProperties.DragDropTypes="{Binding
DataContext.DragDropTypes, RelativeSource={RelativeSource AncestorType={x:Type
Views:ReleaseTracksView}}}" Attached:ListBoxProperties.DragEffects="{Binding
DataContext.DragEffects, RelativeSource={RelativeSource AncestorType={x:Type
Views:ReleaseTracksView}}}">
I must be honest though... this was a lot of work. However, now that I can implement drag and drop operations with visual feedback just by setting a few properties, it totally seems worth it.

Tab Navigation on Virtualized Items Panel

How can you set the Tab Navigation on virtualized items? As example;
<ListBox x:Name="Items">
<ListBox.Template>
<ControlTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel VirtualizingStackPanel.VirtualizationMode="Recycling"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Button />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
If I set TabNavigation=Once or Cycle on the Scrollviewer itself, or the Listbox parent etc, it only tabs through items available in the viewport since the others haven't been generated yet. Is there a trick someone might share for when tabbing through Item Objects it will allow Tab to proceed to the next not-yet-virtualized item while bringing it to view in the viewport and providing intuitive tabbing through the controls?
So basically what was came up (thanks to the extra eyes and help of another fine dev) with was to go ahead and render the other items but while remaining virtualized from onload with a custom behavior and at the same time exposing a dependency for continuous scrolling and bringing the current item into view in the viewport;
namespace The.Namespace
{
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
/// <summary>
/// Scroll selected item into view.
/// </summary>
public class ListBoxFocusBehavior : FocusBehavior<ListBox>
{
public static readonly DependencyProperty IsContinuousProperty = DependencyProperty.Register("IsContinuous",
typeof(bool),
typeof(ListBoxFocusBehavior),
new PropertyMetadata(
false,
(d, e) => ((ListBoxFocusBehavior)d).IsContinuousScroll = (bool)e.NewValue));
/// <summary>
/// Gets or sets a value indicating whether this instance is continuous.
/// </summary>
/// <value>
/// <c>true</c> if this instance is continuous; otherwise, <c>false</c>.
/// </value>
public bool IsContinuous
{
get { return (bool)GetValue(IsContinuousProperty); }
set { SetValue(IsContinuousProperty, value); }
}
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += SelectionChanged;
AssociatedObject.KeyDown += KeyDown;
}
/// <summary>
/// Keys down.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Windows.Input.KeyEventArgs"/> instance containing the event data.</param>
private void KeyDown(object sender, KeyEventArgs e)
{
e.Handled = false;
if (e.Key == Key.Tab && Keyboard.Modifiers == ModifierKeys.None)
{
//forward tab ...
var idx = AssociatedObject.Items.IndexOf(AssociatedObject.SelectedItem);
if (idx < AssociatedObject.Items.Count-1)
{
AssociatedObject.SelectedItem = AssociatedObject.Items[idx + 1];
e.Handled = true;
}
}
if (e.Key == Key.Tab && (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
{
//back tab.
var idx = AssociatedObject.Items.IndexOf(AssociatedObject.SelectedItem);
if (idx > 0)
{
AssociatedObject.SelectedItem = AssociatedObject.Items[idx - 1];
e.Handled = true;
}
}
}
/// <summary>
/// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
/// </summary>
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectionChanged -= SelectionChanged;
AssociatedObject.KeyDown -= KeyDown;
}
/// <summary>
/// Gots the focus.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
private void GotFocus(object sender, RoutedEventArgs e)
{
if (AssociatedObject.SelectedItem == null && AssociatedObject.Items.Any())
{
AssociatedObject.SelectedItem = AssociatedObject.Items.First();
}
}
/// <summary>
/// Selections the changed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Windows.Controls.SelectionChangedEventArgs"/> instance containing the event data.</param>
private void SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (AssociatedObject.SelectedItem == null) return;
AssociatedObject.UpdateLayout();
//have to, otherwise the listbox will probably not focus.
Action setFocus = () =>
{
AssociatedObject.UpdateLayout();
AssociatedObject.ScrollIntoView(AssociatedObject.SelectedItem);
//ensure that if the container did not exist yet (virtualized), it gets created.
AssociatedObject.UpdateLayout();
var container =
AssociatedObject.ItemContainerGenerator.ContainerFromItem(
AssociatedObject.SelectedItem) as Control;
if (container != null)
{
container.Focus();
}
};
AssociatedObject.Dispatcher.BeginInvoke(setFocus);
}
}
}

WPF MVVM Modal Overlay Dialog only over a View (not Window)

I'm pretty much new to the MVVM architecture design...
I was struggling lately to find a suitable control already written for such a purpose but had no luck, so I reused parts of XAML from another similar control and got make my own.
What I want to achieve is:
Have a reusable View (usercontrol) + viewmodel (to bind to) to be able to use inside other views as a modal overlay showing a dialog that disables the rest of the view, and shows a dialog over the it.
How I wanted to implement it:
create a viewmodel that takes string(message) and action+string collection(buttons)
viewmodel creates a collection of ICommands that call those actions
dialog view binds to the its viewmodel that will be exposed as property of another viewmodel (parent)
dialog view is put into the xaml of the parent like this:
pseudoXAML:
<usercontrol /customerview/ ...>
<grid>
<grid x:Name="content">
<various form content />
</grid>
<ctrl:Dialog DataContext="{Binding DialogModel}" Message="{Binding Message}" Commands="{Binding Commands}" IsShown="{Binding IsShown}" BlockedUI="{Binding ElementName=content}" />
</grid>
</usercontrol>
So here the modal dialog gets the datacontext from the DialogModel property of the Customer viewmodel, and binds commands and message. It would be also bound to some other element (here 'content') that needs to be disabled when the dialog shows (binding to IsShown). When you click some button in the dialog the associated command is called that simply calls the associated action that was passed in the constructor of the viewmodel.
This way I would be able to call Show() and Hide() of the dialog on the dialog viewmodel from inside the Customer viewmodel and alter the dialog viewmodel as needed.
It would give me only one dialog at a time but that is fine.
I also think that the dialog viewmodel would remain unittestable, since the unittests would cover the calling of the commands that ought to be created after it being created with Actions in the constructor. There would be a few lines of codebehind for the dialog view, but very little and pretty dumb (setters getters, with almost no code).
What concerns me is:
Is this ok?
Are there any problems I could get into?
Does this break some MVVM principles?
Thanks a lot!
EDIT: I posted my complete solution so you can have a better look. Any architectural comments welcome. If you see some syntax that can be corrected the post is flagged as community wiki.
Well not exactly an answer to my question, but here is the result of doing this dialog, complete with code so you can use it if you wish - free as in free speech and beer:
XAML Usage in another view (here CustomerView):
<UserControl
x:Class="DemoApp.View.CustomerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:DemoApp.View"
>
<Grid>
<Grid Margin="4" x:Name="ModalDialogParent">
<put all view content here/>
</Grid>
<controls:ModalDialog DataContext="{Binding Dialog}" OverlayOn="{Binding ElementName=ModalDialogParent, Mode=OneWay}" IsShown="{Binding Path=DialogShown}"/>
</Grid>
</UserControl>
Triggering from parent ViewModel (here CustomerViewModel):
public ModalDialogViewModel Dialog // dialog view binds to this
{
get
{
return _dialog;
}
set
{
_dialog = value;
base.OnPropertyChanged("Dialog");
}
}
public void AskSave()
{
Action OkCallback = () =>
{
if (Dialog != null) Dialog.Hide();
Save();
};
if (Email.Length < 10)
{
Dialog = new ModalDialogViewModel("This email seems a bit too short, are you sure you want to continue saving?",
ModalDialogViewModel.DialogButtons.Ok,
ModalDialogViewModel.CreateCommands(new Action[] { OkCallback }));
Dialog.Show();
return;
}
if (LastName.Length < 2)
{
Dialog = new ModalDialogViewModel("The Lastname seems short. Are you sure that you want to save this Customer?",
ModalDialogViewModel.CreateButtons(ModalDialogViewModel.DialogMode.TwoButton,
new string[] {"Of Course!", "NoWay!"},
OkCallback,
() => Dialog.Hide()));
Dialog.Show();
return;
}
Save(); // if we got here we can save directly
}
Here is the code:
ModalDialogView XAML:
<UserControl x:Class="DemoApp.View.ModalDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="root">
<UserControl.Resources>
<ResourceDictionary Source="../MainWindowResources.xaml" />
</UserControl.Resources>
<Grid>
<Border Background="#90000000" Visibility="{Binding Visibility}">
<Border BorderBrush="Black" BorderThickness="1" Background="AliceBlue"
CornerRadius="10,0,10,0" VerticalAlignment="Center"
HorizontalAlignment="Center">
<Border.BitmapEffect>
<DropShadowBitmapEffect Color="Black" Opacity="0.5" Direction="270" ShadowDepth="0.7" />
</Border.BitmapEffect>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource ModalDialogHeader}" Text="{Binding DialogHeader}" Grid.Row="0"/>
<TextBlock Text="{Binding DialogMessage}" Grid.Row="1" TextWrapping="Wrap" Margin="5" />
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Grid.Row="2">
<ContentControl HorizontalAlignment="Stretch"
DataContext="{Binding Commands}"
Content="{Binding}"
ContentTemplate="{StaticResource ButtonCommandsTemplate}"
/>
</StackPanel>
</Grid>
</Border>
</Border>
</Grid>
</UserControl>
ModalDialogView code behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace DemoApp.View
{
/// <summary>
/// Interaction logic for ModalDialog.xaml
/// </summary>
public partial class ModalDialog : UserControl
{
public ModalDialog()
{
InitializeComponent();
Visibility = Visibility.Hidden;
}
private bool _parentWasEnabled = true;
public bool IsShown
{
get { return (bool)GetValue(IsShownProperty); }
set { SetValue(IsShownProperty, value); }
}
// Using a DependencyProperty as the backing store for IsShown. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsShownProperty =
DependencyProperty.Register("IsShown", typeof(bool), typeof(ModalDialog), new UIPropertyMetadata(false, IsShownChangedCallback));
public static void IsShownChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue == true)
{
ModalDialog dlg = (ModalDialog)d;
dlg.Show();
}
else
{
ModalDialog dlg = (ModalDialog)d;
dlg.Hide();
}
}
#region OverlayOn
public UIElement OverlayOn
{
get { return (UIElement)GetValue(OverlayOnProperty); }
set { SetValue(OverlayOnProperty, value); }
}
// Using a DependencyProperty as the backing store for Parent. This enables animation, styling, binding, etc...
public static readonly DependencyProperty OverlayOnProperty =
DependencyProperty.Register("OverlayOn", typeof(UIElement), typeof(ModalDialog), new UIPropertyMetadata(null));
#endregion
public void Show()
{
// Force recalculate binding since Show can be called before binding are calculated
BindingExpression expressionOverlayParent = this.GetBindingExpression(OverlayOnProperty);
if (expressionOverlayParent != null)
{
expressionOverlayParent.UpdateTarget();
}
if (OverlayOn == null)
{
throw new InvalidOperationException("Required properties are not bound to the model.");
}
Visibility = System.Windows.Visibility.Visible;
_parentWasEnabled = OverlayOn.IsEnabled;
OverlayOn.IsEnabled = false;
}
private void Hide()
{
Visibility = Visibility.Hidden;
OverlayOn.IsEnabled = _parentWasEnabled;
}
}
}
ModalDialogViewModel:
using System;
using System.Windows.Input;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Windows;
using System.Linq;
namespace DemoApp.ViewModel
{
/// <summary>
/// Represents an actionable item displayed by a View (DialogView).
/// </summary>
public class ModalDialogViewModel : ViewModelBase
{
#region Nested types
/// <summary>
/// Nested enum symbolizing the types of default buttons used in the dialog -> you can localize those with Localize(DialogMode, string[])
/// </summary>
public enum DialogMode
{
/// <summary>
/// Single button in the View (default: OK)
/// </summary>
OneButton = 1,
/// <summary>
/// Two buttons in the View (default: YesNo)
/// </summary>
TwoButton,
/// <summary>
/// Three buttons in the View (default: AbortRetryIgnore)
/// </summary>
TreeButton,
/// <summary>
/// Four buttons in the View (no default translations, use Translate)
/// </summary>
FourButton,
/// <summary>
/// Five buttons in the View (no default translations, use Translate)
/// </summary>
FiveButton
}
/// <summary>
/// Provides some default button combinations
/// </summary>
public enum DialogButtons
{
/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration Ok
/// </summary>
Ok,
/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration OkCancel
/// </summary>
OkCancel,
/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration YesNo
/// </summary>
YesNo,
/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration YesNoCancel
/// </summary>
YesNoCancel,
/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration AbortRetryIgnore
/// </summary>
AbortRetryIgnore,
/// <summary>
/// As System.Window.Forms.MessageBoxButtons Enumeration RetryCancel
/// </summary>
RetryCancel
}
#endregion
#region Members
private static Dictionary<DialogMode, string[]> _translations = null;
private bool _dialogShown;
private ReadOnlyCollection<CommandViewModel> _commands;
private string _dialogMessage;
private string _dialogHeader;
#endregion
#region Class static methods and constructor
/// <summary>
/// Creates a dictionary symbolizing buttons for given dialog mode and buttons names with actions to berform on each
/// </summary>
/// <param name="mode">Mode that tells how many buttons are in the dialog</param>
/// <param name="names">Names of buttons in sequential order</param>
/// <param name="callbacks">Callbacks for given buttons</param>
/// <returns></returns>
public static Dictionary<string, Action> CreateButtons(DialogMode mode, string[] names, params Action[] callbacks)
{
int modeNumButtons = (int)mode;
if (names.Length != modeNumButtons)
throw new ArgumentException("The selected mode needs a different number of button names", "names");
if (callbacks.Length != modeNumButtons)
throw new ArgumentException("The selected mode needs a different number of callbacks", "callbacks");
Dictionary<string, Action> buttons = new Dictionary<string, Action>();
for (int i = 0; i < names.Length; i++)
{
buttons.Add(names[i], callbacks[i]);
}
return buttons;
}
/// <summary>
/// Static contructor for all DialogViewModels, runs once
/// </summary>
static ModalDialogViewModel()
{
InitTranslations();
}
/// <summary>
/// Fills the default translations for all modes that we support (use only from static constructor (not thread safe per se))
/// </summary>
private static void InitTranslations()
{
_translations = new Dictionary<DialogMode, string[]>();
foreach (DialogMode mode in Enum.GetValues(typeof(DialogMode)))
{
_translations.Add(mode, GetDefaultTranslations(mode));
}
}
/// <summary>
/// Creates Commands for given enumeration of Actions
/// </summary>
/// <param name="actions">Actions to create commands from</param>
/// <returns>Array of commands for given actions</returns>
public static ICommand[] CreateCommands(IEnumerable<Action> actions)
{
List<ICommand> commands = new List<ICommand>();
Action[] actionArray = actions.ToArray();
foreach (var action in actionArray)
{
//RelayExecuteWrapper rxw = new RelayExecuteWrapper(action);
Action act = action;
commands.Add(new RelayCommand(x => act()));
}
return commands.ToArray();
}
/// <summary>
/// Creates string for some predefined buttons (English)
/// </summary>
/// <param name="buttons">DialogButtons enumeration value</param>
/// <returns>String array for desired buttons</returns>
public static string[] GetButtonDefaultStrings(DialogButtons buttons)
{
switch (buttons)
{
case DialogButtons.Ok:
return new string[] { "Ok" };
case DialogButtons.OkCancel:
return new string[] { "Ok", "Cancel" };
case DialogButtons.YesNo:
return new string[] { "Yes", "No" };
case DialogButtons.YesNoCancel:
return new string[] { "Yes", "No", "Cancel" };
case DialogButtons.RetryCancel:
return new string[] { "Retry", "Cancel" };
case DialogButtons.AbortRetryIgnore:
return new string[] { "Abort", "Retry", "Ignore" };
default:
throw new InvalidOperationException("There are no default string translations for this button configuration.");
}
}
private static string[] GetDefaultTranslations(DialogMode mode)
{
string[] translated = null;
switch (mode)
{
case DialogMode.OneButton:
translated = GetButtonDefaultStrings(DialogButtons.Ok);
break;
case DialogMode.TwoButton:
translated = GetButtonDefaultStrings(DialogButtons.YesNo);
break;
case DialogMode.TreeButton:
translated = GetButtonDefaultStrings(DialogButtons.YesNoCancel);
break;
default:
translated = null; // you should use Translate() for this combination (ie. there is no default for four or more buttons)
break;
}
return translated;
}
/// <summary>
/// Translates all the Dialogs with specified mode
/// </summary>
/// <param name="mode">Dialog mode/type</param>
/// <param name="translations">Array of translations matching the buttons in the mode</param>
public static void Translate(DialogMode mode, string[] translations)
{
lock (_translations)
{
if (translations.Length != (int)mode)
throw new ArgumentException("Wrong number of translations for selected mode");
if (_translations.ContainsKey(mode))
{
_translations.Remove(mode);
}
_translations.Add(mode, translations);
}
}
#endregion
#region Constructors and initialization
public ModalDialogViewModel(string message, DialogMode mode, params ICommand[] commands)
{
Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, _translations[mode], commands);
}
public ModalDialogViewModel(string message, DialogMode mode, params Action[] callbacks)
{
Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, _translations[mode], CreateCommands(callbacks));
}
public ModalDialogViewModel(string message, Dictionary<string, Action> buttons)
{
Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, buttons.Keys.ToArray(), CreateCommands(buttons.Values.ToArray()));
}
public ModalDialogViewModel(string message, string header, Dictionary<string, Action> buttons)
{
if (buttons == null)
throw new ArgumentNullException("buttons");
ICommand[] commands = CreateCommands(buttons.Values.ToArray<Action>());
Init(message, header, buttons.Keys.ToArray<string>(), commands);
}
public ModalDialogViewModel(string message, DialogButtons buttons, params ICommand[] commands)
{
Init(message, Application.Current.MainWindow.GetType().Assembly.GetName().Name, ModalDialogViewModel.GetButtonDefaultStrings(buttons), commands);
}
public ModalDialogViewModel(string message, string header, DialogButtons buttons, params ICommand[] commands)
{
Init(message, header, ModalDialogViewModel.GetButtonDefaultStrings(buttons), commands);
}
public ModalDialogViewModel(string message, string header, string[] buttons, params ICommand[] commands)
{
Init(message, header, buttons, commands);
}
private void Init(string message, string header, string[] buttons, ICommand[] commands)
{
if (message == null)
throw new ArgumentNullException("message");
if (buttons.Length != commands.Length)
throw new ArgumentException("Same number of buttons and commands expected");
base.DisplayName = "ModalDialog";
this.DialogMessage = message;
this.DialogHeader = header;
List<CommandViewModel> commandModels = new List<CommandViewModel>();
// create commands viewmodel for buttons in the view
for (int i = 0; i < buttons.Length; i++)
{
commandModels.Add(new CommandViewModel(buttons[i], commands[i]));
}
this.Commands = new ReadOnlyCollection<CommandViewModel>(commandModels);
}
#endregion
#region Properties
/// <summary>
/// Checks if the dialog is visible, use Show() Hide() methods to set this
/// </summary>
public bool DialogShown
{
get
{
return _dialogShown;
}
private set
{
_dialogShown = value;
base.OnPropertyChanged("DialogShown");
}
}
/// <summary>
/// The message shown in the dialog
/// </summary>
public string DialogMessage
{
get
{
return _dialogMessage;
}
private set
{
_dialogMessage = value;
base.OnPropertyChanged("DialogMessage");
}
}
/// <summary>
/// The header (title) of the dialog
/// </summary>
public string DialogHeader
{
get
{
return _dialogHeader;
}
private set
{
_dialogHeader = value;
base.OnPropertyChanged("DialogHeader");
}
}
/// <summary>
/// Commands this dialog calls (the models that it binds to)
/// </summary>
public ReadOnlyCollection<CommandViewModel> Commands
{
get
{
return _commands;
}
private set
{
_commands = value;
base.OnPropertyChanged("Commands");
}
}
#endregion
#region Methods
public void Show()
{
this.DialogShown = true;
}
public void Hide()
{
this._dialogMessage = String.Empty;
this.DialogShown = false;
}
#endregion
}
}
ViewModelBase has :
public virtual string DisplayName { get; protected set; }
and implements INotifyPropertyChanged
Some resources to put in the resource dictionary:
<!--
This style gives look to the dialog head (used in the modal dialog)
-->
<Style x:Key="ModalDialogHeader" TargetType="{x:Type TextBlock}">
<Setter Property="Background" Value="{StaticResource Brush_HeaderBackground}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="4" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Margin" Value="5" />
<Setter Property="TextWrapping" Value="NoWrap" />
</Style>
<!--
This template explains how to render the list of commands as buttons (used in the modal dialog)
-->
<DataTemplate x:Key="ButtonCommandsTemplate">
<ItemsControl IsTabStop="False" ItemsSource="{Binding}" Margin="6,2">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button MinWidth="75" Command="{Binding Path=Command}" Margin="4" HorizontalAlignment="Right">
<TextBlock Text="{Binding Path=DisplayName}" Margin="2"></TextBlock>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
I have a custom open source FrameworkElement on my GitHub page that allows you to display modal content over the primary content.
The control can be used like this:
<c:ModalContentPresenter IsModal="{Binding DialogIsVisible}">
<TabControl Margin="5">
<Button Margin="55"
Padding="10"
Command="{Binding ShowModalContentCommand}">
This is the primary Content
</Button>
</TabItem>
</TabControl>
<c:ModalContentPresenter.ModalContent>
<Button Margin="75"
Padding="50"
Command="{Binding HideModalContentCommand}">
This is the modal content
</Button>
</c:ModalContentPresenter.ModalContent>
</c:ModalContentPresenter>
Features:
Displays arbitrary content.
Does not disable the primary content whilst the modal content is being displayed.
Disables mouse and keyboard access to the primary content whilst the modal content is displayed.
Is only modal to the content it is covering, not the entire application.
can be used in an MVVM friendly way by binding to the IsModal property.
I would approach this as a service that gets injected into your ViewModel, along the lines of the sample code below. To the extent what you want to do is in fact message box behavior, I would have my service implementation use a MessageBox!
I am using KISS here in order to present the concept. No code behind, and completely unit testable as shown.
As an aside, that Josh Smith example you are working off of was incredibly helpful to me also, even if it doesn't cover everything
HTH,
Berry
/// <summary>
/// Simple interface for visually confirming a question to the user
/// </summary>
public interface IConfirmer
{
bool Confirm(string message, string caption);
}
public class WPFMessageBoxConfirmer : IConfirmer
{
#region Implementation of IConfirmer
public bool Confirm(string message, string caption) {
return MessageBox.Show(message, caption, MessageBoxButton.YesNo) == MessageBoxResult.Yes;
}
#endregion
}
// SomeViewModel uses an IConfirmer
public class SomeViewModel
{
public ShellViewModel(ISomeRepository repository, IConfirmer confirmer)
{
if (confirmer == null) throw new ArgumentNullException("confirmer");
_confirmer = confirmer;
...
}
...
private void _delete()
{
var someVm = _masterVm.SelectedItem;
Check.RequireNotNull(someVm);
if (detailVm.Model.IsPersistent()) {
var msg = string.Format(GlobalCommandStrings.ConfirmDeletion, someVm.DisplayName);
if(_confirmer.Confirm(msg, GlobalCommandStrings.ConfirmDeletionCaption)) {
_doDelete(someVm);
}
}
else {
_doDelete(someVm);
}
}
...
}
// usage in the Production code
var vm = new SomeViewModel(new WPFMessageBoxConfirmer());
// usage in a unit test
[Test]
public void DeleteCommand_OnExecute_IfUserConfirmsDeletion_RemovesSelectedItemFrom_Workspaces() {
var confirmerMock = MockRepository.GenerateStub<IConfirmer>();
confirmerMock.Stub(x => x.Confirm(Arg<string>.Is.Anything, Arg<string>.Is.Anything)).Return(true);
var vm = new ShellViewModel(_repository, _crudConverter, _masterVm, confirmerMock, _validator);
vm.EditCommand.Execute(null);
Assert.That(vm.Workspaces, Has.Member(_masterVm.SelectedItem));
Assert.That(vm.Workspaces, Is.Not.Empty);
vm.DeleteCommand.Execute(null);
Assert.That(vm.Workspaces, Has.No.Member(_masterVm.SelectedItem));
Assert.That(vm.Workspaces, Is.Empty);
}

Categories

Resources