ComboBox stops updating when clicked / opened - c#

I have a view with 2 ComboBoxes. When an item is selected in the first one, a list is get for the second one. The properties that link the view and view model are as follow:
In the code behind of the view I link the view and view model
var viewModel = new MetrologyFileViewModel();
DataContext = viewModel;
WPF:
<ComboBox ItemsSource="{Binding MetrologyProperties}"
SelectedIndex="{Binding NewSelectedMetrologyPropertyIndex}"
Grid.Column="1"
Grid.ColumnSpan="2"
Style="{StaticResource ComboBoxStyle1}" />
<ComboBox ItemsSource="{Binding NewMetrologyData}"
SelectedIndex="{Binding NewSelectedMetrologyDataIndex}"
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Style="{StaticResource ComboBoxStyle1}" />
C#:
/// <summary>
/// The new metrology property of the metrology data.
/// </summary>
public int NewSelectedMetrologyPropertyIndex
{
get { return _newSelectedMetrologyPropertyIndex; }
set
{
_newSelectedMetrologyPropertyIndex = value;
_newMetrologyData = _newSelectedMetrologyPropertyIndex > 0 ? new ObservableCollection<MetrologyData> { new MetrologyData() }.AddRange(DbServiceMetrologyData.GetOnProperty(MetrologyProperties[_newSelectedMetrologyPropertyIndex])) : null;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(NewMetrologyData));
}
}
/// <summary>
/// The new available metrology data that can be chosen from.
/// </summary>
public ObservableCollection<MetrologyData> NewMetrologyData
{
get { return _newMetrologyData; }
set
{
_newMetrologyData = value;
NotifyPropertyChanged();
}
}
/// <summary>
/// The new metrology data of the metrology fie.
/// </summary>
public int NewSelectedMetrologyDataIndex
{
get { return _newSelectedMetrologyDataIndex; }
set
{
_newSelectedMetrologyDataIndex = value;
NotifyPropertyChanged();
}
}
The AddRange method:
/// <summary>
/// Add a range of items to a <see cref="ObservableCollection{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the <see cref="ObservableCollection{T}"/></typeparam>
/// <param name="sourceCollection">The <see cref="ObservableCollection{T}"/> where the items have to be added to.</param>
/// <param name="newCollection"><see cref="ObservableCollection{T}"/> with the new items.</param>
public static ObservableCollection<T> AddRange<T>(this ObservableCollection<T> sourceCollection, ObservableCollection<T> newCollection)
{
if (newCollection != null)
foreach (var item in newCollection)
sourceCollection.Add(item);
return sourceCollection;
}
The first time I select a property in the 1st ComboBox, the 2nd one updates fine though the view (I think) isn't fully updated because the space where the list should be displayed is very small (see picture).
This keeps working as long as I don't click the 2nd ComboBox. From the moment I open (click on) the 2nd ComboBox, it stops updating when I select a property in the 1st one.
Update
I found out that when just calling DbServiceMetrologyData.GetOnProperty(MetrologyProperties[_newSelectedMetrologyPropertyIndex]) the problem is fixed, I don't know why though.

I found out that when just calling DbServiceMetrologyData.GetOnProperty(MetrologyProperties[_newSelectedMetrologyPropertyIndex]) the problem is fixed, I don't know why though.

Related

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!

WPF two-way binding not working

I have a data context (UserPreferences) assigned to my main window, and a textbox that binds two-way to a property within one of the data context's properties (CollectionDevice) within the context.
When the Window loads, the textbox's do not bind to the properties in my model. I verify within the debugger that the data context is set to the model object and the model's properties are properly assigned. All I get however are a series of textbox's with 0's in them.
When I enter the data into the textbox's, the data is updated in the model. The issue just happens when I load the data and apply it to the data context, the text box does not get updated.
When I save the model to the database, the proper data gets saved from the textbox. When I restore the model from the database, the proper data gets applied. When the model is applied to the data context within my constructor, the textbox's datacontext contains the correct data and it's properties are assigned as they should be. The issue is the UI does not reflect this.
XAML
<Window.DataContext>
<models:UserPreferences />
</Window.DataContext>
<!-- Wrap pannel used to store the manual settings for a collection device. -->
<StackPanel Name="OtherCollectionDevicePanel">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Margin="10, 10, 0, 0" Text="Baud Rate" />
<TextBox Name="BaudRateTextBox" Text="{Binding Path=SelectedCollectionDevice.BaudRate, Mode=TwoWay}" Margin="10, 10, 0, 0" MinWidth="80" ></TextBox>
</StackPanel>
<WrapPanel>
<TextBlock VerticalAlignment="Center" Margin="10, 10, 0, 0" Text="Com Port" />
<TextBox Text="{Binding Path=SelectedCollectionDevice.ComPort, Mode=TwoWay}" Margin="10, 10, 0, 0" MinWidth="80" ></TextBox>
</WrapPanel>
<WrapPanel>
<TextBlock VerticalAlignment="Center" Margin="10, 10, 0, 0" Text="Data Points" />
<TextBox Text="{Binding Path=SelectedCollectionDevice.DataPoints, Mode=TwoWay}" Margin="10, 10, 0, 0" MinWidth="80" ></TextBox>
</WrapPanel>
<WrapPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Margin="10, 10, 0, 0" Text="WAAS" />
<CheckBox IsChecked="{Binding Path=SelectedCollectionDevice.WAAS, Mode=TwoWay}" Content="Enabled" Margin="20, 0, 0, 0" VerticalAlignment="Bottom"></CheckBox>
</WrapPanel>
</StackPanel>
Model <-- Datacontext.
/// <summary>
/// Provides a series of user preferences.
/// </summary>
[Serializable]
public class UserPreferences : INotifyPropertyChanged
{
private CollectionDevice selectedCollectionDevice;
public UserPreferences()
{
this.AvailableCollectionDevices = new List<CollectionDevice>();
var yuma1 = new CollectionDevice
{
BaudRate = 4800,
ComPort = 31,
DataPoints = 1,
Name = "Trimble Yuma 1",
WAAS = true
};
var yuma2 = new CollectionDevice
{
BaudRate = 4800,
ComPort = 3,
DataPoints = 1,
Name = "Trimble Yuma 2",
WAAS = true
};
var toughbook = new CollectionDevice
{
BaudRate = 4800,
ComPort = 3,
DataPoints = 1,
Name = "Panasonic Toughbook",
WAAS = true
};
var other = new CollectionDevice
{
BaudRate = 0,
ComPort = 0,
DataPoints = 0,
Name = "Other",
WAAS = false
};
this.AvailableCollectionDevices.Add(yuma1);
this.AvailableCollectionDevices.Add(yuma2);
this.AvailableCollectionDevices.Add(toughbook);
this.AvailableCollectionDevices.Add(other);
this.SelectedCollectionDevice = this.AvailableCollectionDevices.First();
}
/// <summary>
/// Gets or sets the GPS collection device.
/// </summary>
public CollectionDevice SelectedCollectionDevice
{
get
{
return selectedCollectionDevice;
}
set
{
selectedCollectionDevice = value;
this.OnPropertyChanged("SelectedCollectionDevice");
}
}
/// <summary>
/// Gets or sets a collection of devices that can be used for collecting GPS data.
/// </summary>
[Ignore]
[XmlIgnore]
public List<CollectionDevice> AvailableCollectionDevices { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notifies objects registered to receive this event that a property value has changed.
/// </summary>
/// <param name="propertyName">The name of the property that was changed.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
CollectionDevice <-- Where text box binds to.
/// <summary>
/// CollectionDevice model
/// </summary>
[Serializable]
public class CollectionDevice : INotifyPropertyChanged
{
/// <summary>
/// Gets or sets the COM port.
/// </summary>
private int comPort;
/// <summary>
/// Gets or sets a value indicating whether [waas].
/// </summary>
private bool waas;
/// <summary>
/// Gets or sets the data points.
/// </summary>
private int dataPoints;
/// <summary>
/// Gets or sets the baud rate.
/// </summary>
private int baudRate;
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the COM port.
/// </summary>
public int ComPort
{
get
{
return this.comPort;
}
set
{
this.comPort= value;
this.OnPropertyChanged("ComPort");
}
}
/// <summary>
/// Gets or sets the COM port.
/// </summary>
public bool WAAS
{
get
{
return this.waas;
}
set
{
this.waas = value;
this.OnPropertyChanged("WAAS");
}
}
/// <summary>
/// Gets or sets the COM port.
/// </summary>
public int DataPoints
{
get
{
return this.dataPoints;
}
set
{
this.dataPoints = value;
this.OnPropertyChanged("DataPoints");
}
}
/// <summary>
/// Gets or sets the COM port.
/// </summary>
public int BaudRate
{
get
{
return this.baudRate;
}
set
{
this.baudRate = value;
this.OnPropertyChanged("BaudRate");
}
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notifies objects registered to receive this event that a property value has changed.
/// </summary>
/// <param name="propertyName">The name of the property that was changed.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public override string ToString()
{
return this.Name;
}
}
Can someone point me in the right direction? I assume the issue is my binding in XAML; I can't find it though. I need it to be two-way bound because the data can change at any time during the apps lifetime within the model (database is updated through syncs) and the UI needs to reflect those changes, yet the user can apply changes to the model via the UI.
Update 1
I tried to force the text box databind to be updated, but that did not work as well.
BindingExpression be = this.BaudRateTextBox.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
I also tried setting the UpdateSourceTrigger to PropertyChanged and that did not seem to resolve the problem either.
<TextBox Name="BaudRateTextBox" Text="{Binding Path=SelectedCollectionDevice.BaudRate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="10, 10, 0, 0" MinWidth="80" ></TextBox>
Update 2
I tried to follow along with some documentation from Microsoft and it does not seem to fix the issue. The values still remain 0 when the window loads. The binding is not being updated after I restore the state of the object from the database. Binding is wired up though because as I enter data, the data context is updated. For some reason, it's acting like One-Way when I have it set to Two-Way.
Update 3
I tried to move the code into the window loaded event and out of the constructor but that did not appear to help. Something I found interesting is that the PropertyChanged event does not get fired during the deserialization process. I don't think it matters in this case because the object is fully restored properly and then I just assign it to the data context anyway. I moved the data context out of the XAML and into the WindowLoaded in order to test if the XAML was the problem. The result was the same.
private void WindowLoaded(object sender, RoutedEventArgs e)
{
// Restore our preferences state.
var preferences = new UserPreferenceCommands();
Models.UserPreferences viewModel = new Models.UserPreferences();
// Set up the event handler before we deserialize.
viewModel.PropertyChanged += viewModel_PropertyChanged;
preferences.LoadPreferencesCommand.Execute(viewModel);
// At this point, viewModel is a valid object. All properties are set correctly.
viewModel = preferences.Results;
// After this step, the UI still shows 0's in all of the text boxs. Even though the values are not zero.
this.DataContext = viewModel;
}
// NEVER gets fired from within the WindowLoaded event.
void viewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
MessageBox.Show("Property changed!");
}
// This changes the model properties and is immediately reflected in the UI. Why does this not happen within the WindowLoaded event?
private void TestButtonClickEvent(object sender, RoutedEventArgs e)
{
var context = this.DataContext as Models.UserPreferences;
context.SelectedCollectionDevice.ComPort = 1536;
}
Update 4 - Problem identified
I have identified the problem, but still need a resolution. The whole point of data binding is so that I don't have to perform this manual assignment. Is there something wrong with my INotify implementations?
private void WindowLoaded(object sender, RoutedEventArgs e)
{
// Restore our preferences state.
var preferences = new UserPreferenceCommands();
Models.UserPreferences viewModel = new Models.UserPreferences();
// Set up the event handler before we deserialize.
viewModel.PropertyChanged += viewModel_PropertyChanged;
preferences.LoadPreferencesCommand.Execute(viewModel);
// At this point, viewModel is a valid object. All properties are set correctly.
viewModel = preferences.Results;
// After this step, the UI still shows 0's in all of the text boxs. Even though the values are not zero.
this.DataContext = viewModel;
// SOLUTION: - Setting the actual property causes the UI to be reflected when the window is initialized; setting the actual data context does not. Why? Also note that I set this property and my PropertyChanged event handler still does not fire.
((Models.UserPreferences) DataContext).SelectedCollectionDevice = viewModel.SelectedCollectionDevice;
}
By default, the Text property of TextBox is updated only when the focus on it is lost.
Did you verify it with your DataContext?
If you want to override this behaviour, you have to include the UpdateSourceTrigger property in this way:
Text="{Binding Path=SelectedCollectionDevice.BaudRate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}
Setting UpdateSourceTrigger's value to PropertyChanged, the change is reflected in the TextBox when you change the value of your bound property, as soon as the text changes.
A useful tutorial about the usage of UpdateSourceTrigger property is here.
Alright, I was able to determine the problem and get it resolved. It turned out to be a compilation of things causing this.
First, my model.
UserPreferences <-- MainWindow is data bound to this.
[Serializable]
public class UserPreferences : INotifyPropertyChanged
{
private CollectionDevice selectedCollectionDevice;
public UserPreferences()
{
this.AvailableCollectionDevices = new List<CollectionDevice>();
var yuma1 = new CollectionDevice
{
BaudRate = 4800,
ComPort = 31,
DataPoints = 1,
Name = "Trimble Yuma 1",
WAAS = true
};
var yuma2 = new CollectionDevice
{
BaudRate = 4800,
ComPort = 3,
DataPoints = 1,
Name = "Trimble Yuma 2",
WAAS = true
};
var toughbook = new CollectionDevice
{
BaudRate = 4800,
ComPort = 3,
DataPoints = 1,
Name = "Panasonic Toughbook",
WAAS = true
};
var other = new CollectionDevice
{
BaudRate = 0,
ComPort = 0,
DataPoints = 0,
Name = "Other",
WAAS = false
};
this.AvailableCollectionDevices.Add(yuma1);
this.AvailableCollectionDevices.Add(yuma2);
this.AvailableCollectionDevices.Add(toughbook);
this.AvailableCollectionDevices.Add(other);
this.SelectedCollectionDevice = this.AvailableCollectionDevices.First();
}
/// <summary>
/// Gets or sets the GPS collection device.
/// </summary>
public CollectionDevice SelectedCollectionDevice
{
get
{
return selectedCollectionDevice;
}
set
{
selectedCollectionDevice = value;
if (selectedCollectionDevice.Name == "Other")
{
this.AvailableCollectionDevices[3] = value;
}
this.OnPropertyChanged("SelectedCollectionDevice");
}
}
/// <summary>
/// Gets or sets a collection of devices that can be used for collecting GPS data.
/// </summary>
[Ignore]
[XmlIgnore]
public List<CollectionDevice> AvailableCollectionDevices { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notifies objects registered to receive this event that a property value has changed.
/// </summary>
/// <param name="propertyName">The name of the property that was changed.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
In the setter for the SelectedCollectionDevice I was not looking to see if the selected device was other. All of the other devices (yuma1, panasonic etc) have pre-determined property values that are never changed. When the user selects "Other" the textbox's are displayed and they can manually enter the data. The problem was that when the manually entered data was restored from the database during the window loading, I was not assigning the custom data in SelectedCollectionDevice to the corresponding object in the collection.
During window load, the Combobox.SelectedItem was set to the index of the SelectedCollectionDevice. The Combobox.ItemsSource was set to the AvailableCollectionDevices collection.
this.CollectionDevice.SelectedIndex =
viewModel.AvailableCollectionDevices.IndexOf(
viewModel.AvailableCollectionDevices.FirstOrDefault(
acd => acd.Name == viewModel.SelectedCollectionDevice.Name));
When the above code is executed, the combo box pulls the default object from its data source, which has all of the values set to zero. Within the combo box's SelectionChanged event I assigned the Data Context SelectedCollectionDevice to the zero'd out item associated with the combo box.
private void CollectionDeviceSelected(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0 && e.AddedItems[0] is CollectionDevice)
{
// Assign the view models SelectedCollectionDevice to the device selected in the combo box.
var device = e.AddedItems[0] as CollectionDevice;
((Models.UserPreferences)this.DataContext).SelectedCollectionDevice = device;
// Check if Other is selected. If so, we have to present additional options.
if (device.Name == "Other")
{
OtherCollectionDevicePanel.Visibility = Visibility.Visible;
}
else if (OtherCollectionDevicePanel.Visibility == Visibility.Visible)
{
OtherCollectionDevicePanel.Visibility = Visibility.Collapsed;
}
}
}
So long story short, I added the code above in the setter for the SelectedCollectionDevice to apply the value to the AvailableCollectionDevices List<>. This way, when the combo box has the "Other" value selected, it pulls the value from the collection with the correct data. During deserialization, I am just deserializing the SelectedCollectionDevice and not the List<> which is why the data was always being overwrote when the window first loaded.
This also explains why re-assigning the the data context SelectedCollectionDevice property with the local viewModel.SelectedCollectionDevice was working. I was replacing the zero'd out object associated with the combo box, which had set the data context during the SelectionChanged event. I am not able to set the DataContext in the XAML and remove the manual assignment.
Thanks for all of the help, it helped me narrow down my debugging until I finally resolved the issue. Much appreciated!
Not an answer but wanted to post the code that works on my machine to help OP...
Complete xaml page...
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<StackPanel Name="OtherCollectionDevicePanel">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Margin="10, 10, 0, 0"
Text="Baud Rate" />
<TextBox Name="BaudRateTextBox"
Text="{Binding Path=SelectedCollectionDevice.BaudRate, Mode=TwoWay}"
Margin="10, 10, 0, 0"
MinWidth="80"></TextBox>
</StackPanel>
<WrapPanel>
<TextBlock VerticalAlignment="Center"
Margin="10, 10, 0, 0"
Text="Com Port" />
<TextBox Text="{Binding Path=SelectedCollectionDevice.ComPort, Mode=TwoWay}"
Margin="10, 10, 0, 0"
MinWidth="80"></TextBox>
</WrapPanel>
<WrapPanel>
<TextBlock VerticalAlignment="Center"
Margin="10, 10, 0, 0"
Text="Data Points" />
<TextBox Text="{Binding Path=SelectedCollectionDevice.DataPoints, Mode=TwoWay}"
Margin="10, 10, 0, 0"
MinWidth="80"></TextBox>
</WrapPanel>
<WrapPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Margin="10, 10, 0, 0"
Text="WAAS" />
<CheckBox IsChecked="{Binding Path=SelectedCollectionDevice.WAAS, Mode=TwoWay}"
Content="Enabled"
Margin="20, 0, 0, 0"
VerticalAlignment="Bottom"></CheckBox>
</WrapPanel>
<Button Click="ButtonBase_OnClick" Content="Set ComPort to 11"></Button>
</StackPanel>
</Grid>
</Window>
Complete code behind...
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Xml.Serialization;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new UserPreferences();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
((UserPreferences) DataContext).SelectedCollectionDevice.ComPort = 11;
}
}
/// <summary>
/// Provides a series of user preferences.
/// </summary>
[Serializable]
public class UserPreferences : INotifyPropertyChanged
{
private CollectionDevice selectedCollectionDevice;
public UserPreferences()
{
this.AvailableCollectionDevices = new List<CollectionDevice>();
var yuma1 = new CollectionDevice
{
BaudRate = 4800,
ComPort = 31,
DataPoints = 1,
Name = "Trimble Yuma 1",
WAAS = true
};
var yuma2 = new CollectionDevice
{
BaudRate = 4800,
ComPort = 3,
DataPoints = 1,
Name = "Trimble Yuma 2",
WAAS = true
};
var toughbook = new CollectionDevice
{
BaudRate = 4800,
ComPort = 3,
DataPoints = 1,
Name = "Panasonic Toughbook",
WAAS = true
};
var other = new CollectionDevice
{
BaudRate = 0,
ComPort = 0,
DataPoints = 0,
Name = "Other",
WAAS = false
};
this.AvailableCollectionDevices.Add(yuma1);
this.AvailableCollectionDevices.Add(yuma2);
this.AvailableCollectionDevices.Add(toughbook);
this.AvailableCollectionDevices.Add(other);
this.SelectedCollectionDevice = this.AvailableCollectionDevices.First();
}
/// <summary>
/// Gets or sets the GPS collection device.
/// </summary>
public CollectionDevice SelectedCollectionDevice
{
get
{
return selectedCollectionDevice;
}
set
{
selectedCollectionDevice = value;
this.OnPropertyChanged("SelectedCollectionDevice");
}
}
/// <summary>
/// Gets or sets a collection of devices that can be used for collecting GPS data.
/// </summary>
[XmlIgnore]
public List<CollectionDevice> AvailableCollectionDevices { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notifies objects registered to receive this event that a property value has changed.
/// </summary>
/// <param name="propertyName">The name of the property that was changed.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
/// <summary>
/// CollectionDevice model
/// </summary>
[Serializable]
public class CollectionDevice : INotifyPropertyChanged
{
/// <summary>
/// Gets or sets the COM port.
/// </summary>
private int comPort;
/// <summary>
/// Gets or sets a value indicating whether [waas].
/// </summary>
private bool waas;
/// <summary>
/// Gets or sets the data points.
/// </summary>
private int dataPoints;
/// <summary>
/// Gets or sets the baud rate.
/// </summary>
private int baudRate;
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the COM port.
/// </summary>
public int ComPort
{
get
{
return this.comPort;
}
set
{
this.comPort = value;
this.OnPropertyChanged("ComPort");
}
}
/// <summary>
/// Gets or sets the COM port.
/// </summary>
public bool WAAS
{
get
{
return this.waas;
}
set
{
this.waas = value;
this.OnPropertyChanged("WAAS");
}
}
/// <summary>
/// Gets or sets the COM port.
/// </summary>
public int DataPoints
{
get
{
return this.dataPoints;
}
set
{
this.dataPoints = value;
this.OnPropertyChanged("DataPoints");
}
}
/// <summary>
/// Gets or sets the COM port.
/// </summary>
public int BaudRate
{
get
{
return this.baudRate;
}
set
{
this.baudRate = value;
this.OnPropertyChanged("BaudRate");
}
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notifies objects registered to receive this event that a property value has changed.
/// </summary>
/// <param name="propertyName">The name of the property that was changed.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public override string ToString()
{
return this.Name;
}
}
}
I had the same issue. My problem was binding property name was wrong. If you look at the output window you can see all the binding errors during runtime.
System.Windows.Data Error: 40 : BindingExpression path error:
'SelectedProtectedWebsiteTemplate' property not found on 'object'
''ProtectedWebsitesViewModel' (HashCode=32764015)'.
BindingExpression:Path=SelectedProtectedWebsiteTemplate.Name;
DataItem='ProtectedWebsitesViewModel' (HashCode=32764015); target
element is 'TextBox' (Name=''); target property is 'Text' (type
'String')

Bindings inside WPF user control

In order to get into the WPF world and getting used to bindings, I've made a user control used to define a search filter. Depending on the wanted filter, the user can either enter a text, pick a date or select an item in a combo box. Here's an example with three instances of the created search control, each being of different type:
The good news is, everything is working but I'm not sure if everything has been done as intended.
SearchUserControl.xaml:
<UserControl x:Class="Zefix.View.UserControls.SearchUserControl"
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"
mc:Ignorable="d"
d:DesignHeight="82"
d:DesignWidth="300"
Height="Auto"
x:Name="SearchUserControlRoot">
<Grid>
<StackPanel>
<Label Name="LabelHeaderText" Content="{Binding HeaderText, ElementName=SearchUserControlRoot}" />
<TextBox Name="TextBoxSearchText" Text="{Binding SearchValue, ElementName=SearchUserControlRoot}" Visibility="{Binding TextBoxVisiblity, ElementName=SearchUserControlRoot}" />
<DatePicker Name="DatePickerSearch" SelectedDate="{Binding SearchValue, ElementName=SearchUserControlRoot}" Visibility="{Binding DatePickerVisiblity, ElementName=SearchUserControlRoot}" />
<ComboBox Name="ComboBoxSearch" Text="{Binding SearchValue, ElementName=SearchUserControlRoot}" ItemsSource="{Binding AvailableValues, ElementName=SearchUserControlRoot}" Visibility="{Binding ComboBoxVisiblity, ElementName=SearchUserControlRoot}" IsEditable="True" />
</StackPanel>
</Grid>
</UserControl>
SearchUserControl.xaml.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using Zefix.DataAccess;
namespace Zefix.View.UserControls {
/// <summary>
/// Interaction logic for SearchUserControl.xaml
/// </summary>
public partial class SearchUserControl {
#region Public Dependency Properties
/// <summary>
/// The search value property
/// </summary>
public static readonly DependencyProperty SearchValueProperty =
DependencyProperty.Register("SearchValue", typeof (object), typeof (SearchUserControl));
/// <summary>
/// The available values property
/// </summary>
public static readonly DependencyProperty AvailableValuesProperty =
DependencyProperty.Register("AvailableValues", typeof (IEnumerable<object>), typeof (SearchUserControl));
/// <summary>
/// The search type property
/// </summary>
public static readonly DependencyProperty SearchTypeProperty =
DependencyProperty.Register("SearchType", typeof (SearchType), typeof (SearchUserControl));
/// <summary>
/// The header text property
/// </summary>
public static readonly DependencyProperty HeaderTextProperty =
DependencyProperty.Register("HeaderText", typeof (string), typeof (SearchUserControl));
#endregion
#region Private Dependency Properties
/// <summary>
/// The combo box visiblity property
/// </summary>
private static readonly DependencyProperty ComboBoxVisiblityProperty =
DependencyProperty.Register("ComboBoxVisiblity", typeof (Visibility), typeof (SearchUserControl));
/// <summary>
/// The text box visiblity property
/// </summary>
private static readonly DependencyProperty TextBoxVisiblityProperty =
DependencyProperty.Register("TextBoxVisiblity", typeof (Visibility), typeof (SearchUserControl));
/// <summary>
/// The date picker visiblity property
/// </summary>
private static readonly DependencyProperty DatePickerVisiblityProperty =
DependencyProperty.Register("DatePickerVisiblity", typeof (Visibility), typeof (SearchUserControl));
#endregion
#region Public Properties
/// <summary>
/// Gets or sets the type of the search.
/// </summary>
/// <value>
/// The type of the search.
/// </value>
public SearchType SearchType {
get { return (SearchType) GetValue(SearchTypeProperty); }
set { SetValue(SearchTypeProperty, value); }
}
/// <summary>
/// Gets or sets the header text.
/// </summary>
/// <value>
/// The header text.
/// </value>
public string HeaderText {
get { return (string) GetValue(HeaderTextProperty); }
set { SetValue(HeaderTextProperty, value); }
}
/// <summary>
/// Gets or sets the available values.
/// </summary>
/// <value>
/// The available values.
/// </value>
public IEnumerable<object> AvailableValues {
get { return (IEnumerable<object>) GetValue(AvailableValuesProperty); }
set { SetValue(AvailableValuesProperty, value); }
}
/// <summary>
/// Gets or sets the search value.
/// </summary>
/// <value>
/// The search value.
/// </value>
public object SearchValue {
get { return GetValue(SearchValueProperty); }
set { SetValue(SearchValueProperty, value); }
}
#endregion
#region Private Properties
/// <summary>
/// Gets or sets the combo box visiblity.
/// </summary>
/// <value>
/// The combo box visiblity.
/// </value>
private Visibility ComboBoxVisiblity {
get { return (Visibility) GetValue(ComboBoxVisiblityProperty); }
set { SetValue(ComboBoxVisiblityProperty, value); }
}
/// <summary>
/// Gets or sets the date picker visiblity.
/// </summary>
/// <value>
/// The date picker visiblity.
/// </value>
private Visibility DatePickerVisiblity {
get { return (Visibility) GetValue(DatePickerVisiblityProperty); }
set { SetValue(DatePickerVisiblityProperty, value); }
}
/// <summary>
/// Gets or sets the text box visiblity.
/// </summary>
/// <value>
/// The text box visiblity.
/// </value>
private Visibility TextBoxVisiblity {
get { return (Visibility) GetValue(TextBoxVisiblityProperty); }
set { SetValue(TextBoxVisiblityProperty, value); }
}
#endregion
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="SearchUserControl" /> class.
/// </summary>
public SearchUserControl() {
InitializeComponent();
DependencyPropertyDescriptor pd = DependencyPropertyDescriptor.FromProperty(SearchTypeProperty, typeof (SearchUserControl));
pd.AddValueChanged(this, OnSearchTypePropertyChanged);
// Initialize default parameters
SearchType = SearchType.Unknown;
}
#endregion
#region Private Methods
/// <summary>
/// Called when the search type property has changed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
private void OnSearchTypePropertyChanged(object sender, EventArgs e) {
// Hide all editors
DatePickerVisiblity = Visibility.Collapsed;
ComboBoxVisiblity = Visibility.Collapsed;
TextBoxVisiblity = Visibility.Collapsed;
// Make the correct editor visible
switch (SearchType) {
case SearchType.Date:
DatePickerVisiblity = Visibility.Visible;
break;
case SearchType.TextSelection:
ComboBoxVisiblity = Visibility.Visible;
break;
case SearchType.Text:
TextBoxVisiblity = Visibility.Visible;
break;
}
}
#endregion
}
}
Instantiation of the search controls from the parent control:
<ribbon:Tab Label="Search">
<ribbon:Group Padding="0,5,0,5">
<customcontrols:SearchUserControl x:Name="SearchUserControlCompanyName" HeaderText="company name" Margin="5,0,0,0" SearchType="Text" VerticalAlignment="Center" VerticalContentAlignment="Center" />
<customcontrols:SearchUserControl x:Name="SearchUserControlCompanyNationality" HeaderText="company nationality (ISO3 code)" Margin="5,0,0,0" SearchType="TextSelection" AvailableValues="{Binding Path=CompaniesViewModel.ISO3Codes}" VerticalAlignment="Center" />
<customcontrols:SearchUserControl x:Name="SearchUserControlDateFounded" HeaderText="date founded" Margin="5,0,0,0" SearchType="Date" VerticalAlignment="Center" VerticalContentAlignment="Center" />
<ribbon:Button Context="StatusBarItem" Name="ButtonApplyFilter" Label="Search" ImageSourceSmall="/Resources/search_magnifying_glass_find.png" Margin="5,0,0,0" VerticalAlignment="Center" Click="OnButtonApplyFilterClicked" Command="{Binding Path=ApplyFilterCommand}" ScreenTipHeader="Apply the search filter" VerticalContentAlignment="Center" VariantSize="Large" />
</ribbon:Group>
</ribbon:Tab>
In the SearchControl I wanted to display the correct component (textbox, datepicker or combobox) according to the set SearchType. For this the, xxxVisibility dependency properties and properties have been created (they are being set when the SearchTypeProperty notifies a property changed event). As there is no reason to expose them as public (they are being used only inside the SearchControl), I've made them private; MSDN states that bound properties MUST be public though. The project compiles and runs without an issue, but errors are being shown for the bound xxxVisibility properties with the message 'Public member expected' (can't tell if it's visual studio or resharper telling me that).
Is my approach to create this user control correct in respect to the WPF concepts?
Should the xxxVisibility properties be public (event though I don't want to expose them)?
This is a very difficult question to 'answer', rather than just 'comment' on. In my personal opinion, your UserControl has been written well and as far as I can see doesn't break any rules. Although I don't see any problem with declaring a private DependencyProperty, it is unusual. In this situation, developers often chose to implement a public Read Only DependencyProperty with a private DependencyPropertyKey instead:
private static readonly DependencyPropertyKey ComboBoxVisiblityPropertyKey
= DependencyProperty.RegisterReadOnly("ComboBoxVisiblity", typeof(int),
typeof(SearchUserControl), new PropertyMetadata(Visibility.Collapsed));
public static readonly DependencyProperty ComboBoxVisiblityProperty
= ComboBoxVisiblityPropertyKey.DependencyProperty;
public int ComboBoxVisiblity
{
get { return (int)GetValue(ComboBoxVisiblityProperty); }
protected set { SetValue(ComboBoxVisiblityPropertyKey, value); }
}
Some developers may also think it unusual that you are creating properties of type Visibility rather than binding bool values with BoolToVisibilityConverters, but again... that is your prerogative. Overall, well done! :)

Binding to a Separate DataContext

I have a MainWindow that contains a window with a TreeView. The tree view binds to an observable collection that I set in the DataContext.
<TreeView ItemsSource="{Binding Trees}" Name="fileTree" MouseDoubleClick="FileTreeMouseDoubleClick" SelectedValuePath="NodePath">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:TreeNodeViewModel}" ItemsSource="{Binding Children}">
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
However, I want to put a separate tree as a child of the MainWindow, which will bind to a different object, how can I do that If I have already used the DataContext property of the MainWindow.xaml?
EDIT: Extension to Question
Now I have:
<TreeView Name="viewTree" ItemsSource="{Binding ViewListTrees, Source=viewListTreeViewModel}">
Where viewListTreeViewModel is a member variable in MainWindow.xaml.cs:
private ViewListTreeViewModel viewListTreeViewModel;
which has the following accessor:
public ObservableCollection<ViewListTreeNodeViewModel> ViewListTrees
{
get { return this.tree; }
}
and ViewListTreeNodeViewModel has:
public string NodeName { get; }
public string NodeImage { get; }
My hierarchical data template now looks like:
<HierarchicalDataTemplate DataType="{x:Type local:ViewListTreeNodeViewModel}" ItemsSource="{Binding Children}">
<StackPanel>
<Image Source="{Binding NodeImage}" />
<TextBlock Text="{Binding NodeName}"/>
</StackPanel>
</HierarchicalDataTemplate>
Simply expose two Properties on a class you bind to your Window (rather than binding the collection directly), exposing ObservableCollection properties; Trees and SeperateTree and bind each TreeView accordingly:
<Window>
<Grid>
<TreeView ItemsSource="{Binding Trees}">
...
</TreeView>
<TreeView ItemsSource="{Binding SeperateTree}">
...
</TreeView>
</Grid>
</Window>
You can either bind directly to the object using the Source part of the Binding or you can set another DataContext locally on the TreeView.
Example 1
<TreeView ItemsSource="{Binding Trees, Source=YourOtherDataContext}"/>
Example 2
<TreeView ItemsSource="{Binding Trees}"
DataContext="{Binding Path=YourOtherDataContext}"/>
As promised in the comments, here is an example of a base class to use for your view models.
Usage
public string Name
{
get { return name; }
set { SetValue(ref name, value, "Name"); }
}
ObservableObject
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Compares the value and sets iff it has changed.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="field">The field.</param>
/// <param name="value">The value.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns><c>True</c> if the field was changed</returns>
protected virtual bool SetValue<T>(ref T field, T value, string propertyName)
{
return SetValue(ref field, value, propertyName, true);
}
/// <summary>
/// Compares the value and sets iff it has changed.
/// </summary>
/// <param name="field">The field.</param>
/// <param name="value">The value.</param>
/// <param name="propertyName">Name of the property.</param>
/// <param name="checkForEquality">if set to <c>true</c> [check for equality].</param>
/// <returns><c>True</c> if the field was changed</returns>
protected virtual bool SetValue<T>(ref T field, T value, string propertyName, bool checkForEquality)
{
if (checkForEquality && EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Sets the value.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="setAction">The set action.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns></returns>
protected virtual bool SetValue(Action setAction, string propertyName)
{
return SetValue(setAction, null, propertyName);
}
/// <summary>
/// Sets the value.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="setAction">The set action.</param>
/// <param name="equalityFunc">The equality func.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns></returns>
protected virtual bool SetValue(Action setAction, Func<bool> equalityFunc, string propertyName)
{
if (equalityFunc != null && !equalityFunc.Invoke())
return false;
setAction.Invoke();
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(this, propertyName);
}
protected void OnPropertyChanged(object source, string propertyName)
{
// copying the event handlers before that this is "thread safe"
// http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}

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