WPF, Datagrid with restricted input - c#

recently I have started a new project in c#- Fireworks planning software. I am quite experienced in WinForms, but I want to learn WPF. So I have passed a couple of tutorials, but still on very beginning.
The task is to make a datagrid, where user puts data for each Firing point, thus for instance
Cue, EffectTime, DelayBeforeEffect, FiringTime and Name (it is not complete). Main window preview
No the thing is the time (EffectTime etc.) has to be input by user via keyboard and I want to:
if the value is like sss.fff (simple number or decimal number)
the program converts it automatically to TimeSpan (h:mm:ss.fff),
which is actually resolution of My firing system. Example: number
65.528 is converted to 0:01:05.528.
negative values are also allowed, because there are several
things have to be done before main pyrotechnics show starts
I would like validate user input it is valid time value, not for
instance 65.528.20. If the value is not correct I need either cancel
user leave the editedcell or at least rewrite the new value with
last validated
there is some relationship between values, for instance: FiringPoint
= EffectTime - Delay, or better FiringPoint = EffectTime.Subtract(Delay);
As I am used to I have created a UserControl TimeText with only one element - TextBox and I check, eventually correct, input upon points listed above.
Then I have created DataGrid and DataGridTemplateColumn in MainWindow,
classes like
FiringPoint (with properties listeda above),
FiringPointManager with ObservableCollection<FiringPoint> implementing INotifyChange interface
Data Binding between Manager and DataGrid.
After facing VisualStudio for about 20 hours, it is really working, but it seems to be very complicated.
Because for instance I need to forward DataGridRow.Background color to my TimeText control via dependencyProperty etc.
So finally my question: Is my approach correct or can somebody suggest better approach how to reach the same functionality?
Thanks for your opinions
Jan
Code for TimeTextBox
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace DataGridHratky
{
/// <summary>
/// Interakční logika pro TimeTextBox.xaml
/// </summary>
public partial class TimeTextBox : UserControl, INotifyPropertyChanged
{
#region Overrides
#endregion
#region Enum
#endregion
#region constants
private const string __NEG = "-";
private static readonly string[] __SEP = { ".", "," };
public const string __TFORMAT = #"%h\:mm\:ss\.fff";
private const string __COL = ":";
#endregion
#region private declarations
private bool _isNegative = false;
private double _onlySecondsValue = 0;
private Brush _okBackground;
private Brush _nokBackground = Brushes.OrangeRed;
private Brush _negBackground = Brushes.LavenderBlush;
private TimeSpan _tsValue = new TimeSpan();
private bool _bOnlySeconds = false;
private string _originalValue;
#endregion
#region private methods
private void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
/// <summary>
/// Determines if value is only in second format. May be also negative and decimal either with "." or ","
/// </summary>
/// <param name="text">text to check</param>
/// <param name="seconds">Out variable, where seconds should be stored</param>
/// <returns>true if so</returns>
private bool onlySeconds(string text, out double seconds)
{
if (Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator == __SEP[0])
text = text.Replace(__SEP[1], __SEP[0]);
else text = text.Replace(__SEP[0], __SEP[1]);
if (Double.TryParse(text, out seconds))
{
_bOnlySeconds = true;
return true;
} else
{
_bOnlySeconds = false;
return false;
}
}
private bool validate(string time)
{
string _rawValue = time;
if (onlySeconds(_rawValue, out _onlySecondsValue))
{
return true;
}
//check if it is negative
if (_rawValue.StartsWith(__NEG))
{
_isNegative = true;
}
else _isNegative = false;
if (TimeSpan.TryParse(_rawValue, out _tsValue)) return true;
else return false;
}
/// <summary>
/// Determines time based on:
/// - if there is only one : in string means minutes and seconds
/// - if there are two : means hours and minutes and seconds
/// - in both cases milliseconds after . or , may be present
/// </summary>
/// <param name="sTime">String representing validated time</param>
/// <returns>Time span with translated time</returns>
private TimeSpan determineTime(string sTime)
{
int _iColon = Regex.Matches(sTime,__COL).Count;
string _sResult;
TimeSpan _tsDays = new TimeSpan();
if (TimeSpan.TryParse(sTime, out _tsDays))
{
if (_tsDays.Days > 0)
return new TimeSpan(0, _tsDays.Hours, _tsDays.Minutes, _tsDays.Seconds, _tsDays.Milliseconds);
}
TimeSpan _ts = new TimeSpan();
if (_iColon == 1) //minutes and seconds
{
//add 0 hours and 0 days
_sResult = addTimeToTimeSpan(sTime, "0.0:");
}
else if
(_iColon == 2) //hours minutes and seconds
{
//add 0 days
_sResult = addTimeToTimeSpan(sTime, "0.");
}
else _sResult = sTime;
if (TimeSpan.TryParse(_sResult, out _ts))
{
// in all cases remove day
return new TimeSpan(0, _ts.Hours, _ts.Minutes, _ts.Seconds, _ts.Milliseconds);
}
else return _ts;
}
/// <summary>
/// Returns time with added value, moves __NEG sign if present to the from
/// </summary>
/// <param name="sTime">Original time</param>
/// <param name="sTimeToAdd">Time to add</param>
/// <returns>string with complete time</returns>
private string addTimeToTimeSpan(string sTime, string sTimeToAdd)
{
string _sResult;
if (sTime.StartsWith(__NEG))
{
_sResult = __NEG + sTimeToAdd + sTime.Remove(0, 1);
}
else _sResult = sTimeToAdd + sTime;
return _sResult;
}
#endregion
#region Public events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region delegates
#endregion
#region Public delcarations
#region Dependency
public Brush BKColor
{
get { return (Brush)GetValue(BKColorProperty); }
set { SetValue(BKColorProperty, value); }
}
// Using a DependencyProperty as the backing store for BKColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BKColorProperty =
DependencyProperty.Register("BKColor", typeof(Brush), typeof(TimeTextBox), new PropertyMetadata(null));
public bool ReadOnly
{
get { return (bool)GetValue(ReadOnlyProperty); }
set {
SetValue(ReadOnlyProperty, value);
timeTextBox.ReadOnly = value;
}
}
// Using a DependencyProperty as the backing store for ReadOnly. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ReadOnlyProperty =
DependencyProperty.Register("ReadOnly", typeof(bool), typeof(TimeTextBox), new PropertyMetadata(null));
public string TimeText
{
get { return (string)GetValue(TimeTextProperty); }
set {
SetValue(TimeTextProperty, value);
//OnPropertyChanged("TimeText");
}
}
public static readonly DependencyProperty TimeTextProperty =
DependencyProperty.Register("TimeText", typeof(string), typeof(TimeTextBox),
new PropertyMetadata(string.Empty, OnTextPropertyChanged));
private static void OnTextPropertyChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
TimeTextBox myUserControl = dependencyObject as TimeTextBox;
myUserControl.OnPropertyChanged("TimeText");
myUserControl.OnTextPropertyChanged(e);
}
private void OnTextPropertyChanged(DependencyPropertyChangedEventArgs e)
{
txtTime.Text = TimeText;
}
public string Time
{
get { return txtTime.Text; }
set { txtTime.Text = value; }
}
#endregion
/// <summary>
/// True if time span is negative
/// </summary>
public bool IsNegative
{
get
{
return _isNegative;
}
}
/// <summary>
/// Gets or sets validated value background color
/// </summary>
public Brush ValueOKBackground
{
get
{
return _okBackground;
}
set
{
_okBackground = value;
}
}
/// <summary>
/// Gets or sets background color if value is not succesfully validated
/// </summary>
public Brush ValueNotOKBackground
{
get
{
return _okBackground;
}
set
{
_nokBackground = value;
}
}
#endregion
#region Public methods
#endregion
#region Constructors
public TimeTextBox()
{
InitializeComponent();
_okBackground = txtTime.Background;
}
#endregion
private void txtTime_TextChanged(object sender, TextChangedEventArgs e)
{
if (validate(txtTime.Text)) txtTime.Background = BKColor;
else
txtTime.Background = _nokBackground;
}
private void txtTime_LostFocus(object sender, RoutedEventArgs e)
{
//leave if nothing changed
if (txtTime.Text == _originalValue) return;
if (validate(txtTime.Text))
{
if (_bOnlySeconds)
{
int seconds = (int)Math.Truncate(_onlySecondsValue);
int milliseconds = (int)((_onlySecondsValue - seconds) * 1000);
_tsValue = new TimeSpan(0, 0, 0, seconds, milliseconds);
}
else
{
_tsValue = determineTime(txtTime.Text);
}
}
else
{
if (!validate(_originalValue)) //validate methods uses _tsValue to put validated timespan
{
_tsValue = new TimeSpan();
}
}
string _sign = _isNegative ? "-" : "";
txtTime.Text = _sign + _tsValue.ToString(__TFORMAT);
txtTime.Background = _isNegative ? _negBackground : BKColor;
TimeText = txtTime.Text;
txtTime.Background = BKColor;
OnPropertyChanged("UpdateTime");
}
private void txtTime_GotFocus(object sender, RoutedEventArgs e)
{
_originalValue = txtTime.Text;
if (!(validate(txtTime.Text))) txtTime.Text = "";
}
}
}
TimeText XAML
<UserControl x:Name="timeTextBox" x:Class="DataGridHratky.TimeTextBox"
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:DataGridHratky"
mc:Ignorable="d"
d:DesignHeight="30" d:DesignWidth="100">
<!-- DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TimeTextBox}}-->
<Grid x:Name="grdLayout">
<TextBox x:Name="txtTime" TextChanged="txtTime_TextChanged" LostFocus="txtTime_LostFocus" GotFocus="txtTime_GotFocus" BorderThickness="0"
Text="{Binding Path=TimeText, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TimeTextBox}}}"
IsReadOnly="{Binding Path=ReadOnly, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TimeTextBox}}}"
Background="{Binding Path=BKColor, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TimeTextBox}}}"
VerticalContentAlignment="Center"/>
</Grid>
</UserControl>
Main Window XAML
<Window x:Class="DataGridHratky.MainWindow"
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:DataGridHratky"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<DataGrid x:Name="dgData" AutoGenerateColumns="False" AddingNewItem="dgData_AddingNewItem" BeginningEdit="dgData_BeginningEdit" CellEditEnding="dgData_CellEditEnding" RowHeight="25" AlternationCount="2" VerticalContentAlignment="Center" >
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<Trigger Property="AlternationIndex" Value="0">
<Setter Property="Background" Value="White" />
</Trigger>
<Trigger Property="AlternationIndex" Value="1">
<Setter Property="Background" Value="WhiteSmoke" />
</Trigger>
<DataTrigger Binding="{Binding Path=Selectable}" Value="False">
<DataTrigger.Setters>
<Setter Property="Background" Value="White" />
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridComboBoxColumn x:Name="colCue" SelectedItemBinding="{Binding SelectedCue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="{Binding AvaiableCues}" Width="50" Header="Cue"/>
<DataGridTextColumn x:Name="colCaption" Width="200" Header="Popis" Binding="{Binding Caption}" />
<DataGridTemplateColumn ClipboardContentBinding="{x:Null}" Header="Čas efektu" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:TimeTextBox x:Name="ttbFireEffect" TimeText="{Binding Effect, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
BKColor="{Binding Path=Background, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridComboBoxColumn x:Name="colType" SelectedItemBinding="{Binding SelectedType, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="{Binding AvaiableFWTypes}" Header="Typ" Width="75" />
<DataGridTemplateColumn ClipboardContentBinding="{x:Null}" Header="Prodleni">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:TimeTextBox x:Name="ttbDelay" TimeText="{Binding Delay, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ReadOnly = "{Binding IsDelayReadOnly}"
BKColor="{Binding Path=Background, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn ClipboardContentBinding="{x:Null}" Header="Čas odpalu" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:TimeTextBox x:Name="ttbFirePoint" TimeText="{Binding FiringPoint, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
BKColor="{Binding Path=Background, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
FirepointManager Code
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataGridHratky
{
public class FirepointsManager : INotifyPropertyChanged
{
#region Enum
#endregion
#region constants
#endregion
#region private declarations
private List<string> _avaiableFirepoints = new List<string>();
private List<string> _avaiableFireworks = new List<string>();
#endregion
#region private methods
#endregion
#region Public events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Protected
protected void RaiseChange(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
#endregion
#region delegates
#endregion
#region Public properties
/// <summary>
/// Firepoints Observable collection
/// </summary>
public ObservableCollection<FirePoint> FirePoints { get; set; }
/// <summary>
/// List of avaiable firepoints
/// </summary>
public List<string>AvaiableCues
{
get
{
return _avaiableFirepoints;
}
}
/// <summary>
/// List of pyrotechnics types
/// </summary>
public List<string>AvaiableFWTypes
{
get
{
return _avaiableFireworks;
}
}
#endregion
#region Public methods
/// <summary>
/// Adds a new Firepoint
/// </summary>
/// <param name="f">Firepoint</param>
public void AddFirePoint(FirePoint f)
{
f.SelectedCueChanged += F_SelectedCueChanged;
FirePoints.Add(f);
}
/// <summary>
/// Checks avaible cues and eliminates __MS if already used
/// </summary>
public void CheckSelectedCues()
{
foreach (var FirePoint in FirePoints)
{
if (FirePoint.SelectedCue == FirePoint.__MS)
{
if (_avaiableFirepoints.Contains(FirePoint.__MS))
_avaiableFirepoints.Remove(FirePoint.__MS);
return;
}
}
if (!_avaiableFirepoints.Contains(FirePoint.__MS))
_avaiableFirepoints.Add(FirePoint.__MS);
RaiseChange("CuesAvaiable");
}
private void F_SelectedCueChanged(object sender, EventArgs e)
{
CheckSelectedCues();
}
#endregion
#region Constructors
public FirepointsManager()
{
FirePoints = new ObservableCollection<FirePoint>();
FirePoints.CollectionChanged += FirePoints_CollectionChanged;
_avaiableFirepoints = FirePoint.GeneratedCues();
_avaiableFireworks = FirePoint.FireworksTypes();
}
private void FirePoints_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
CheckSelectedCues();
}
#endregion
}
}

Related

WPF Data bindings not working after intiailize. No Binding Errors

I am having an issue with the data bindings between my View and ViewModel. The bindings only run at initialize and then they will not update. The code runs as expected and I have no binding errors per the output window, but the bindings on the UI will not update.
Program Setup:
WPF
C#
Prism V8.1.97
MVVM
.NET Framework 4.7.2
Things I have tried:
XAML set to bind directly to the property with INotifyPropertyChanged
XAML set to Find the RelativeSource of type UserControl
RelayCommand to update the UI, with and without Invoke to the main thread.
BackgroundWorker to update the UI, with and without Invoke to the main thread.
DelegateCommand to update the UI, with and without Invoke to the main thread.
i.Interaction.EventTriggers with Click to Invoke a UI update on the main thread.
Everyone of these will run the code, but will not update the UI. I have left some of the code that I have tried in the program for BackgroundWorker, Delegate void, and Dispatcher.BeginInvoke. I have written a few programs and have never had this issue. Did I setup something wrong when I created the program?
UPDATE: There seems to be an issue with Visual Studio 2019. This might be only on my version as the other programs I have written no longer work. This could would normally run as intended.
UserControl I am trying to do a simple binding with. I created a Textbox at the bottom with Mode=TwoWay to see if the TextBlock would update.
<UserControl x:Class="MRC.Module.ModStatus.Views.ModStatusManagerView"
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:MRC.Module.ModStatus.Views"
xmlns:prism="http://prismlibrary.com/"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:cconv="clr-namespace:MRC.Framework.Core.Data.Converters;assembly=MRC.Framework.Core"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" >
<UserControl.Resources>
<cconv:RemoveSpacesConverter x:Key="IntToStringConvert"/>
</UserControl.Resources>
<Grid>
<Grid.Background>
<SolidColorBrush Color="#222222"/>
</Grid.Background>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<Border BorderBrush="DodgerBlue" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<Border BorderThickness="1" BorderBrush="DodgerBlue" VerticalAlignment="Stretch" Height="{Binding ActualHeight, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}}}">
<StackPanel VerticalAlignment="Stretch">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Height="20" Margin="0,6" Content="{Binding StartStop}" Width="100"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click" >
<i:InvokeCommandAction Command="{Binding ProgressCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Margin="5,0" HorizontalAlignment="Center" Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.Health, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource IntToStringConvert}}" FontSize="16" Foreground="White"/>
<TextBlock Margin="5,0" Text="{Binding TestingText}" Foreground="White" FontSize="16" AutomationProperties.Name="TestText"/>
</StackPanel>
<!--
Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.Health, UpdateSourceTrigger=PropertyChanged}"
-->
<ProgressBar Maximum="100" Minimum="0"
VerticalAlignment="Bottom" Height="25" Margin="5" />
<TextBox Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.TestingText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" FontSize="16" Height="30" Margin="5" Width="100"/>
</StackPanel>
</Border>
</Grid>
</Grid>
</Border>
<TextBlock HorizontalAlignment="Center" Grid.Column="1" VerticalAlignment="Center" Text="Mod Status"
FontSize="30" Foreground="Black"/>
</Grid>
</Grid>
</UserControl>
Code Behind
public partial class ModStatusManagerView : UserControl
{
public ModStatusManagerView()
{
InitializeComponent();
DataContext = new ModStatusManagerViewModel();
}
}
ViewModel
public class ModStatusManagerViewModel : ViewModelBase
{
#region Variables
private readonly BackgroundWorker worker = new BackgroundWorker();
private delegate void UpdateUIDelgate(string health, string Status);
#endregion
#region Commands
public ICommand ProgressCommand { get; private set; }
private void Testing(string health, string Status)
{
try
{
}
catch(Exception ex)
{
System.Windows.MessageBox.Show(ex.Message);
}
}
private bool CanProgressExecute()
{
return true;
}
private void Progress()
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
Health = 0;
StartStop = "Stop";
TestingText = "Initialized";
for (int i = 0; i < 99; i++)
{
Health = i;
System.Windows.Application.Current.MainWindow.UpdateLayout();
System.Threading.Thread.Sleep(100);
}
TestingText = "Completed";
System.Windows.MessageBox.Show("Complete");
}, System.Windows.Threading.DispatcherPriority.Render);
/*if (!System.Windows.Application.Current.Dispatcher.CheckAccess())
{
System.Windows.Application.Current.Dispatcher.BeginInvoke(
new UpdateUIDelgate(Testing), "Stop", "Initialized");
return;
}*/
// System.Windows.Application.Current.MainWindow.Dispatcher.BeginInvoke(
// new UpdateUIDelgate(Testing), "Stop", "Initialized");
}
catch(Exception ex)
{
System.Windows.MessageBox.Show(ex.Message);
}
}
public ICommand ProgressOffCommand { get; }
private void ProgressOff()
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
StartStop = "Start";
});
}
#endregion
#region Constructor
public ModStatusManagerViewModel()
{
this.ProgressCommand = new RelayCommand(Progress);
//this.ProgressOffCommand = new RelayCommand(ProgressOff);
}
#endregion
#region Properties
public bool IsEnabled
{
get { return _isEnabled; }
set { SetProperty(ref _isEnabled, value); }
}
private bool _isEnabled = true;
/// <summary>
/// Gets or Sets the Max Health
/// </summary>
public string StartStop
{
get { return _startStop; }
set { SetProperty(ref _startStop, value); }
}
private string _startStop = "Start";
/// <summary>
/// Gets or Sets the Max Health
/// </summary>
public bool IsOn
{
get { return _isOn; }
set { SetProperty(ref _isOn, value); }
}
private bool _isOn = false;
/// <summary>
/// Gets or Sets the Max Health
/// </summary>
public double MaxHealth
{
get { return _maxHealth; }
set { SetProperty(ref _maxHealth, value); }
}
private double _maxHealth = 100;
/// <summary>
/// Gets or Sets the Min Health
/// </summary>
public double MinHealth
{
get { return _minHealth; }
set { SetProperty(ref _minHealth, value); }
}
private double _minHealth = 0;
/// <summary>
/// Gets or Sets the Min Health
/// </summary>
public double Health
{
get { return _Health; }
set { SetProperty(ref _Health, value); }
}
private double _Health = 0;
public string TestingText
{
get { return _testingText; }
set { SetProperty(ref _testingText, value); }
}
private string _testingText = "Waiting";
#endregion
#region Events
#endregion
#region Methods
private void worker_DoWork(object sender, DoWorkEventArgs e)
{
while (true)
{
while (IsOn)
{
Random rnd = new Random();
Health = rnd.Next(0, 100);
System.Threading.Thread.Sleep(150);
}
System.Threading.Thread.Sleep(150);
}
}
private void worker_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
//update ui once worker complete his work
}
#endregion
}
In case someone would like to see the implementation of the INotifyPropertyChanged
public abstract class ViewModelBase : ModelBase
{
public ViewModelBase()
{
}
~ViewModelBase()
{
}
public bool HasAnyErrors { get; set; }
}
Here is the ModelBase that ViewModelBase Implements
public abstract class ModelBase : BindableBase, INotifyPropertyChanged
{
protected ModelBase()
{
}
~ModelBase() { }
public bool HasIssues { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public static event PropertyChangedEventHandler StaticPropertyChanged = delegate { };
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public static void OnStaticPropertyChanged(string propertyName)
{
StaticPropertyChanged?.Invoke(
typeof(ViewModelBase),
new PropertyChangedEventArgs(propertyName));
}
}
Here is the RelayCommand
public class RelayCommand : ICommand
{
#region Private Members
private Action _action;
private Action<object> _actionOb;
Action<object> _execteMethod;
Func<object, bool> _canexecuteMethod;
#endregion
#region Public Events
/// <summary>
/// Basic Command
/// </summary>
public event EventHandler CanExecuteChanged = (sender, e) => { };
#endregion
#region Constructors
/// <summary>
/// Default Constructor
/// </summary>
/// <param name="action"></param>
public RelayCommand(Action action)
{
_action = action;
}
/*public RelayCommand(System.Collections.ObjectModel.ObservableCollection<Frames.Model.Category> category, Action<object> action)
{
_actionOb = action;
}*/
/// <summary>
/// Default Constructor that passes an object
/// </summary>
/// <param name="execteMethod"></param>
/// <param name="canexecuteMethod"></param>
public RelayCommand(Action<object> execteMethod, Func<object, bool> canexecuteMethod)
{
_execteMethod = execteMethod;
_canexecuteMethod = canexecuteMethod;
}
/// <summary>
/// Default Constructor that determines if an action can execute before executing
/// </summary>
/// <param name="action"></param>
/// <param name="CanExecute"></param>
public RelayCommand(Action action, bool CanExecute)
{
if (CanExecute)
return;
_action = action;
}
public RelayCommand(Action<object> action, bool CanExecuteOb)
{
_actionOb = action;
}
#endregion
#region Command Methods
/// <summary>
/// Returns True if bool Parameter is not busy
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
public bool CanExecute(object parameter)
{
if (parameter != null)
{
try
{
if ((bool)parameter)
return false;
return true;
}
catch
{
return true;
}
}
else
{
return true;
}
}
public bool CanExecuteOb(object parameter)
{
return true;
}
/// <summary>
/// Executes an Action that is not busy
/// </summary>
/// <param name="parameter"></param>
public void Execute(object parameter)
{
Task.Run(async () =>
{
_action();
});
}
/// <summary>
/// Executes an Action that is not busy with a <href="Param">
/// </summary>
/// <param name="param"></param>
public void ExecuteOb(object param)
{
Task.Run(async () =>
{
_actionOb(param);
});
}
#endregion
}
UPDATE: It seems the functionality of SetProperty has stopped working.
I changed my properties from
public string TestingText
{
get { return _testingText; }
set { SetProperty(ref _testingText, value);}
}
private string _testingText = "Testing";
To
public string TestingText
{
get { return _testingText; }
set
{
if (_testingText == value)
return;
_testingText = value;
OnPropertyChanged("TestingText");
}
}
private string _testingText = "Testing";
everything is now working as it should be.

How to show TreeView on ComboBox.SelectedItem != null

I am writing a Directory explorer using the MVVM-Pattern.
My UI consists of a ComboBox which contains a path to a directory and a TreeView which acts as a Directory explorer.
The thing I am struggling with is to show the TreeView only when a ComboBox item is selected but I have no real idea how to achieve that.
Here is my code.
MainWindow.xaml
<StackPanel>
<TextBlock TextWrapping="Wrap" Text="Select a Repository Directory" Margin="10,0" FontSize="16"/>
<ComboBox x:Name="cmbx" Margin="10,30,10,0" ItemsSource="{Binding CmbxItems}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"/>
<TreeView x:Name="FolderView" Height="250" Margin="10,50,10,0" ItemsSource="{Binding Items}" Visibility="{Binding IsItemSelected}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Name="img" Width="20" Margin="5"
Source="{Binding Type,
Converter={x:Static local:HeaderToImageConverter.ConverterInstance}}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
ViewModel.cs
/// <summary>
/// ViewModel for the main Directory view.
/// </summary>
class ViewModel : BaseViewModel
{
#region Properties
public ObservableCollection<DirectoryStructureViewModel> Items { get; set; }
public ObservableCollection<string> CmbxItems { get; set; }
public bool _itemIsSelected = false;
public bool ItemIsSelected
{
get
{
return _itemIsSelected;
}
set
{
_itemIsSelected = value;
}
}
public string _selectedItem;
public string SelectedItem
{
get
{
return _selectedItem;
}
set
{
ItemIsSelected = true;
MessageBox.Show("Selection changed");
_selectedItem = value;
}
}
#endregion
#region Constructor
/// <summary>
/// Constructor.
/// </summary>
public ViewModel()
{
CmbxItems = new ObservableCollection<string>(){};
CmbxItems.Add(DirectoryItem.rootPath);
//Get initial directory.
var children = DirectoryStructure.GetInitialDirectory();
//Create view models from data.
this.Items = new ObservableCollection<DirectoryStructureViewModel>(children.Select(dir => new DirectoryStructureViewModel(dir.FullPath, NodeTypes.Folder)));
}
#endregion
}
BaseViewModel.cs
/// <summary>
/// Base ViewModel that fires PropertyChanged events.
/// </summary>
public class BaseViewModel : INotifyPropertyChanged
{
//Event that is fired when aa child property changes its value.
public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
}
DirectoryStructureViewModel.cs
public class DirectoryStructureViewModel : BaseViewModel
{
#region Properties
public NodeTypes Type { get; set; }
public string FullPath { get; set; }
public string Name { get { return DirectoryStructure.GetDirectoryOrFileName(this.FullPath); } }
/// <summary>
/// List of all children contained in this item.
/// </summary>
public ObservableCollection<DirectoryStructureViewModel> Children { get; set; }
/// <summary>
/// Indicates that this item can be expanded.
/// </summary>
public bool CanExpand { get { return this.Type != NodeTypes.File; } }
/// <summary>
/// Indicates if the current item is expanded.
/// </summary>
public bool IsExpanded
{
get
{
return this.Children?.Count(chldrn => chldrn != null) > 0;
}
set
{
if (value == true)
{
Expand();
}
else
{
this.ClearChildren();
}
}
}
public bool IsItemSelected { get; set; }
#endregion
#region Commands
public ICommand ExpandCommand { get; set; }
#endregion
#region Constructor
/// <summary>
/// Constructor.
/// </summary>
/// <param name="fullPath">Path of this item.</param>
/// <param name="type">Type of this item.</param>
public DirectoryStructureViewModel(string fullPath, NodeTypes type)
{
this.ExpandCommand = new TreeViewRelayCommand(Expand);
this.FullPath = fullPath;
this.Type = type;
this.ClearChildren();
}
#endregion
#region Helper Methods
//Removes all children from the list.
private void ClearChildren()
{
this.Children = new ObservableCollection<DirectoryStructureViewModel>();
if (this.Type != NodeTypes.File)
{
//Adds a dummy item to show the expand arrow.
this.Children.Add(null);
}
}
#endregion
#region Functions
/// <summary>
/// Expands this directory and finds all children.
/// </summary>
private void Expand()
{
if (this.Type != NodeTypes.File)
{
//Find all children
var children = DirectoryStructure.GetDirectoryContents(this.FullPath);
this.Children = new ObservableCollection<DirectoryStructureViewModel>(children.Select(content => new DirectoryStructureViewModel(content.FullPath, content.Type)));
}
else
{
return;
}
}
#endregion
}
Commands.cs
class TreeViewRelayCommand : ICommand
{
#region Members
private Action mAction;
#endregion
#region Events
/// <summary>
/// Event that is executed, when <see cref="CanExecute(object)"/> value has changed.
/// </summary>
public event EventHandler CanExecuteChanged = (sender, e) => { };
#endregion
#region Constructor
/// <summary>
/// Constructor.
/// </summary>
/// <param name="action"></param>
public TreeViewRelayCommand(Action action)
{
mAction = action;
}
#endregion
#region Command Methods
public bool CanExecute(object parameter)
{
return true;
}
/// <summary>
/// Executes the commands action.
/// </summary>
/// <param name="parameter"></param>
public void Execute(object parameter)
{
mAction();
}
#endregion
}
Edit: I am using FodyWeavers
You are almost there:
<TreeView ... Visibility="{Binding IsItemSelected}">
One problem is that you are binding Visibility to bool. There should be an binding error in Outputs window (check it now and always for various possible problems with WPF applications).
You can use BoolToVisibilityConverter (using this answer):
<someContainer.Resources>
<BooleanToVisibilityConverter x:Key="converter" />
</someContainer.Resources>
...
<TreeView ... Visibility="{Binding IsItemSelected, Converter={StaticResource converter}}">
Backing fields shouldn't be public.
You should (normally) rise notifications for all properties used in bindings. Typically at the end of setter.
I'd personally use getter only property:
string _selectedItem;
public string SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsItemSelected));
}
}
public bool IsItemSelected => SelectedItem != null;
Also you miss correct event rising method:
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// can be public if you want to rise event from outside
protected void OnPropertyChanged([CallerMemberName] string property = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}

WPF OpenFileDialog using MVVM (Model-View-ViewModel) in c#

How to write WPF OpenFileDialog using MVVM (Model-View-ViewModel) in c#?
I have visited some websites regarding this OpenFileDialog ,but I didn't get clear idea about it. This is my code
In View.xaml:
<Window.... xmlns:VM="clr-namespace:myproject.myViewModel"
... >
<Window.DataContext><VM:myViewModel/>
</Window.DataContext>
<ItemsControl ItemsSource="{Binding mygroup}" >
<ItemsControl.ItemTemplate >
<DataTemplate>
<Grid >....
<TextBlock Text="Color" Margin="20" VerticalAlignment="Center"/>
<ComboBox KeyboardNavigation.TabIndex="0" Grid.Column="1" Margin="45,10,10,10" Height="30" Width="200" ItemsSource="{Binding Color}" />
<TextBlock Text="Shapes" Grid.Row="1" VerticalAlignment="Center" />
<ComboBox KeyboardNavigation.TabIndex="3" Grid.Column="1" Grid.Row="1" Height="20" Width="150" SelectedIndex="0" HorizontalContentAlignment="Right"
VerticalAlignment="Center" ItemsSource="{Binding Shapes}">
</ComboBox>
<TextBlock Text="Open Files " VerticalAlignment="Center" Grid.Row="0" Grid.Column="2" Margin="10" />
<TextBox Grid.Column="3" Text="" Height="30" Grid.Row="0" IsReadOnly="True"
TextAlignment="Right" VerticalContentAlignment="Center" Width="200" /> <Button Grid.Column="4" Content="Browse" Height="30" VerticalAlignment="Bottom" MinWidth="41" />
</Grid>
</Window>
In Model.cs:
namespace Myproject.Models
{
public class ProjectModel : INotifyPropertyChanged
{
private ObservableCollection<string> color;
private ObservableCollection<string> shapes;
public ObservableCollection<string> Color
{
get { return color; }
set
{
color = value;
NotifyPropertyChanged("Color");
}
}
public ObservableCollection<string> Shapes
{
get { return shapes; }
set
{
shapes = value;
NotifyPropertyChanged("Shapes");
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Private Helpers
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
In ViewModel.cs:
namespace MyProject.ViewModels
{
public class myProjectViewModel : INotifyPropertyChanged
{
private ObservableCollection<myprojectmodel> mygroup;
public ObservableCollection<myprojectmodel> Mygroup
{
get => this.mygroup;
set
{
if (Equals(value, this.mygroup)) return;
this.mygroup = value;
OnPropertyChanged();
}
}
public ProjectViewModel()
{
Mygroup = new ObservableCollection<myprojectmodel>();
List<string> lstColor = new List<string>();
lstCity = new List<string> {"Pink", "Blue", "Red"};
List<string> lstShapes = new List<string>();
lstTemperature = new List<string> {"Rectangle", "Triangle", "Circle"};
Mygroup.Add(
new ProjectModel
{
Color= new ObservableCollection<string>(lstColor),
Shapes = new ObservableCollection<string>(lstShapes),
});
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
How should I write code to get OpenFileDialog.
I have seen this link WPF OpenFileDialog with the MVVM pattern? but I don't know how to write it for my above code.
please someone help me by editing my code to get OpenFileDailog.
In my opinion this kind of things doesn't belong in to the ViewModel. It's View specific logic. The View alone handles user input and then sends it to the ViewModel. ViewModel never asks the View to do something. This would invert the dependency chain and couple the ViewModel to the View. The dependencies have to be like this:
View --> ViewModel --> Model. The ViewModel doesn't even know about the type of views nor that there is a View at all.
You have to open the dialog from your view and then send the result to the view model.
For this you could create a simple event handler in your code-behind and attach it to a button's click event. You take the picked file and use an ICommand to invoke e.g. an open file action. That's MVVM (or MVP). Separate the concerns of the views from your models.
MainWindow.xaml:
<Window x:Class="WpfOpenDialogExample.OpenFileDialogSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="OpenFileDialogSample" Height="300" Width="300">
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<Grid>
<Button Name="ShowFilePickerButton" Click="ShowFilePicker_OnClick" Content="Open file" />
</Grid>
</Window>
MainWindow.xaml.cs:
using System;
using System.IO;
using System.Windows;
using Microsoft.Win32;
namespace WpfOpenDialogExample
{
public partial class OpenFileDialogSample : Window
{
public OpenFileDialogSample()
{
InitializeComponent();
}
private void ShowFilePicker_OnClick(object sender, RoutedEventArgs e)
{
var viewModel = this.DataContext as ViewModel;
OpenFileDialog openFileDialog = new OpenFileDialog();
if(openFileDialog.ShowDialog() == true && viewModel.OpenFileCommand.CanExecute(openFileDialog.FileName))
{
viewModel.OpenFileCommand.Execute(openFileDialog.FileName);
}
}
private void ShowFolderPicker_OnClick(object sender, RoutedEventArgs e)
{
var viewModel = this.DataContext as ViewModel;
FolderBrowserDialog openFolderDialog = new FolderBrowserDialog();
if(openFolderDialog.ShowDialog() == DialogResul.Ok && viewModel.OpenFolderCommand.CanExecute(openFolderDialog.SelectedPath ))
{
viewModel.OpenFolderCommand.Execute(openFolderDialog.SelectedPath );
}
}
}
}
ViewModel.cs:
public ICommand OpenFileCommand { get => new RelayCommand(OpenFile, CanOpenFile); }
private void OpenFile(string filePath)
{
...
}
private bool CanOpenFile(string filePath)
{
return File.Exists(filePath);
}
public ICommand OpenFolderCommand { get => new RelayCommand(OpenFolder, CanOpenFolder); }
private void OpenFolder(string folderPath)
{
...
}
private bool CanOpenFolder(string folderPath)
{
return Directory.Exists(filePath);
}
RelayCommand.cs:
using System;
using System.Windows.Input;
namespace WpfOpenDialogExample
{
/// <summary>
/// An implementation independent ICommand implementation.
/// Enables instant creation of an ICommand without implementing the ICommand interface for each command.
/// The individual Execute() an CanExecute() members are suplied via delegates.
/// <seealso cref="System.Windows.Input.ICommand"/>
/// </summary>
/// <remarks>The type of <c>RelaisCommand</c> actually is a <see cref="System.Windows.Input.ICommand"/></remarks>
public class RelayCommand : ICommand
{
/// <summary>
/// Default constructor to declare the concrete implementation of Execute(object):void and CanExecute(object) : bool
/// </summary>
/// <param name="executeDelegate">Delegate referencing the execution context method.
/// Delegate signature: delegate(object):void</param>
/// <param name="canExecuteDelegate">Delegate referencing the canExecute context method.
/// Delegate signature: delegate(object):bool</param>
public RelayCommand(Action<object> executeDelegate , Predicate<object> canExecuteDelegate)
{
this.executeDelegate = executeDelegate;
this.canExecuteDelegate = canExecuteDelegate;
}
/// <summary>
/// Invokes the custom <c>canExecuteDelegate</c> which should check wether the command can be executed.
/// </summary>
/// <param name="parameter">Optional parameter of type <see cref="System.Object"/></param>
/// <returns>Expected to return tue, when the preconditions meet the requirements and therefore the command stored in <c>executeDelegate</c> can execute.
/// Expected to return fals when command execution is not possible.</returns>
public bool CanExecute(object parameter)
{
if (this.canExecuteDelegate != null)
{
return this.canExecuteDelegate(parameter);
}
return false;
}
/// <summary>
/// Invokes the custom <c>executeDelegate</c>, which references the command to execute.
/// </summary>
/// <param name="parameter">Optional parameter of type <see cref="System.Object"/></param>
public void Execute(object parameter)
{
if (this.executeDelegate != null)
this.executeDelegate(parameter);
}
/// <summary>
/// The event is triggered every time the conditions regarding the command have changed. This occures when <c>InvalidateRequerySuggested()</c> gets explicitly or implicitly called.
/// Triggering this event usually results in an invokation of <c>CanExecute(object):bool</c> to check if the occured change has made command execution possible.
/// The <see cref="System.Windows.Input.CommandManager"/> holds a weakrefernce to the observer.
/// </summary>
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
private readonly Action<object> executeDelegate;
private readonly Predicate<object> canExecuteDelegate;
}
}

How to change UI language using resource dictionary at run time?

I want to change my project language by button_click event, so I use ResourceDictionary to do it like this:
XAML
<Button Content="{DynamicResource LanguageSetting}" Click="btn_LanguageSetting_Click"/>
Code Behind
public static string windowCurrentLanguageFile = "Language/en.xaml";
private void btn_LanguageSetting_Click(object sender, RoutedEventArgs e)
{
windowCurrentLanguageFile = windowCurrentLanguageFile == "Language/en.xaml"
? "Language/fr.xaml"
: "Language/en.xaml";
var rd = new ResourceDictionary() { Source = new Uri(windowCurrentLanguageFile, UriKind.RelativeOrAbsolute) };
if (this.Resources.MergedDictionaries.Count == 0)
this.Resources.MergedDictionaries.Add(rd);
else
this.Resources.MergedDictionaries[0] = rd;
}
This works fine for the xaml file, but I also want to change my viewmodel's language in the code behind.
My ItemsControl in the xaml:
<ItemsControl ItemsSource="{Binding ItemOperate}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewmodel:SelectableViewModel}">
<Border x:Name="Border" Padding="0,8,0,8" BorderThickness="0 0 0 1" BorderBrush="{DynamicResource MaterialDesignDivider}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Checkerz" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ToggleButton VerticalAlignment="Center" IsChecked="{Binding IsSelected}"
Style="{StaticResource MaterialDesignActionLightToggleButton}"
Content="{Binding Code}" />
<StackPanel Margin="8 0 0 0" Grid.Column="7">
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource MaterialDesignSelection}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Which Binding to the ViewModel like this:
public class SelectableViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected == value) return;
_isSelected = value;
OnPropertyChanged();
}
}
private char _code;
public char Code
{
get { return _code; }
set
{
if (_code == value) return;
_code = value;
OnPropertyChanged();
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
OnPropertyChanged();
}
}
private string _description;
public string Description
{
get { return _description; }
set
{
if (_description == value) return;
_description = value;
OnPropertyChanged();
}
}
}
And
public MainViewModel()
{
_itemOperate = CreateData();
}
private static ObservableCollection<SelectableViewModel> CreateData()
{
return new ObservableCollection<SelectableViewModel>
{
new SelectableViewModel
{
Code = 'E',
Name = "Erase",
Description = "Erase The MCU Chip By Page"
},
new SelectableViewModel
{
Code = 'D',
Name = "Detect",
Description = "Detect The MCU Flash",
},
new SelectableViewModel
{
Code = 'P',
Name = "Programming",
Description = "Programming The MCU Chip By Hex File",
},
new SelectableViewModel
{
Code = 'V',
Name = "Verify",
Description = "Verify The Downing Code",
},
new SelectableViewModel
{
Code ='L',
Name = "Lock",
Description = "Lock The Code To Protect The MCU",
}
};
}
So how I suppose to change the language my model SelectableViewModel Code, Name, Description instead of using this hard code? Thanks!
I will propose you a solution for changing the language at runtime.
Create the resource manager:
public class CultureResources
{
private static bool isAvailableCulture;
private static readonly List<CultureInfo> SupportedCultures = new List<CultureInfo>();
private static ObjectDataProvider provider;
public CultureResources()
{
GetAvailableCultures();
}
/// <summary>
/// Gets automatically all supported cultures resource files.
/// </summary>
public void GetAvailableCultures()
{
if (!isAvailableCulture)
{
var appStartupPath = AppDomain.CurrentDomain.BaseDirectory;
foreach (string dir in Directory.GetDirectories(appStartupPath))
{
try
{
DirectoryInfo dirinfo = new DirectoryInfo(dir);
var culture = CultureInfo.GetCultureInfo(dirinfo.Name);
SupportedCultures.Add(culture);
}
catch (ArgumentException)
{
}
}
isAvailableCulture = true;
}
}
/// <summary>
/// Retrieves the current resources based on the current culture info
/// </summary>
/// <returns></returns>
public Resources GetResourceInstance()
{
return new Resources();
}
/// <summary>
/// Gets the ObjectDataProvider wrapped with the current culture resource, to update all localized UIElements by calling ObjectDataProvider.Refresh()
/// </summary>
public static ObjectDataProvider ResourceProvider
{
get {
return provider ??
(provider = (ObjectDataProvider)System.Windows.Application.Current.FindResource("Resources"));
}
}
/// <summary>
/// Changes the culture
/// </summary>
/// <param name="culture"></param>
public void ChangeCulture(CultureInfo culture)
{
if (SupportedCultures.Contains(culture))
{
Resources.Culture = culture;
ResourceProvider.Refresh();
}
else
{
var ci = new CultureInfo("en");
Resources.Culture = ci;
ResourceProvider.Refresh();
}
}
/// <summary>
/// Sets english as default language
/// </summary>
public void SetDefaultCulture()
{
CultureInfo ci = new CultureInfo("en");
Resources.Culture = ci;
ResourceProvider.Refresh();
}
/// <summary>
/// Returns localized resource specified by the key
/// </summary>
/// <param name="key">The key in the resources</param>
/// <returns></returns>
public static string GetValue(string key)
{
if (key == null) throw new ArgumentNullException();
return Resources.ResourceManager.GetString(key, Resources.Culture);
}
/// <summary>
/// Sets the new culture
/// </summary>
/// <param name="cultureInfo"> new CultureInfo("de-DE"); new CultureInfo("en-gb");</param>
public void SetCulture(CultureInfo cultureInfo)
{
//get language short format - {de} {en}
var ci = new CultureInfo(cultureInfo.Name.Substring(0,2));
ChangeCulture(ci);
Thread.CurrentThread.CurrentCulture = cultureInfo;
Thread.CurrentThread.CurrentUICulture = cultureInfo;
// CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-GB");
}
}
Define resource file for each language type as you want.
above I've defined the default for English and the second file for German (you can see the ".de" extension. So you do for any other language.
Be sure that you open the Resources.resx properties and select as Custom Tool the value PublicResXFileCodeGenerator. This step is necessary in order to expose the Resource through an object provider.
Register the Resource provider via an object provider in the App.xaml:
Usage:
in Xaml :
Text="{Binding ResourcesText1, Source={StaticResource Resources}}"
From *.cs : Resources.ResourcesText1.
P.S.
When you change the culture be sure you call SetCulture method from the CultureResouces.

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