WPF Binding: disable TextBoxes + Validation and DataContext Issue - c#

My simple program has two windows:
from the first one I set a Boolean value, which then...
I'll use in the second window to disable a number of TextBoxes depending on the aforementioned value itself.
Said TextBoxes are also characterized by a validation binding. At now my validation task is flawlessly working, but I'm not able to make the binding to the IsEnabled TextBox property work.
This is the snippet of my XAML containing one of the TextBoxes (for now the only one I've bound):
<TextBox x:Name="tbSlave1" Validation.Error="ValidationError" IsEnabled="{Binding TextBoxEnabled}" Text="{Binding UpdateSourceTrigger=PropertyChanged, Path=SlavePoint1Name, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"/>
While this is my second window class:
public partial class GeneratorWindow : Window, INotifyPropertyChanged
{
private readonly Validator validator = new Validator();
private int noOfErrorsOnScreen;
public GeneratorWindow()
{
this.InitializeComponent();
this.grid.DataContext = this.validator;
}
public int NumberOfPoints { private get; set; }
public int MainPDC { private get; set; }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private Boolean IsEnabled;
public Boolean TextBoxEnabled
{
get { return IsEnabled; }
set
{
IsEnabled = value;
NotifyPropertyChanged("TextBoxEnabled");
}
}
private void ValidationError(object sender, ValidationErrorEventArgs eventArgs)
{
if (eventArgs.Action == ValidationErrorEventAction.Added)
{
this.noOfErrorsOnScreen++;
}
else
{
this.noOfErrorsOnScreen--;
}
}
private void ValidationCanBeExecuted(object sender, CanExecuteRoutedEventArgs eventArgs)
{
eventArgs.CanExecute = this.noOfErrorsOnScreen == 0;
eventArgs.Handled = true;
}
private void ValidationExecuted(object sender, ExecutedRoutedEventArgs eventArgs)
{
// If the validation was successful, let's generate the files.
this.Close();
eventArgs.Handled = true;
}
}
For now, what I'm getting back is that my window is disabled (can't select any TextBox) and, obviously, this:
System.Windows.Data Error: 40 : BindingExpression path error: 'TextBoxEnabled' property not found on 'object' ''Validator' (HashCode=14499481)'. BindingExpression:Path=TextBoxEnabled; DataItem='Validator' (HashCode=14499481); target element is 'TextBox' (Name='tbSlave1'); target property is 'IsEnabled' (type 'Boolean')
From what I can understand, the culprit is the way I'm managing the DataContext in my class constructor. I probably need to add something to the validator line or totally change it, but I can't understand how.

I think you should be fine if you set the DataContext to the GeneratorWindow and updated you bindings accordingly. For that to work you need to change Validator to a public property.
Changed Validator definition:
public Validator Validator { get; } = new Validator();
DataContext:
this.grid.DataContext = this;
Updated Binding:
<TextBox x:Name="tbSlave1"
Validation.Error="ValidationError"
IsEnabled="{Binding TextBoxEnabled}"
Text="{Binding UpdateSourceTrigger=PropertyChanged,
Path=Validator.SlavePoint1Name,
ValidatesOnDataErrors=true,
NotifyOnValidationError=true}"/>

Related

WPF DataGrid, ObservableCollection Updates, and Redrawing

I will try to keep this as brief as possible, but there is a fair amount of nuance to this question.
The Workflow
I am working in C# and am using WPF and MVVM for the UI for an addin for Revit (a 3D modeling software from Autodesk).
The overarching goal is to create a window that shows the parameters of a 3D element after it is selected. This is a filtered list that are specific to my organization and our users needs, and allow the user to edit them in order to streamline their workflow. The complication is that because I am working with an API I can only use what tools I am given when interacting with the model.
The issue I am running into lies in the workflow. I have detailed the workflow below.
User Selects a 3D Element
The addin uses the API to pull the parameters and wraps them in a wrapper class and adds them into a custom ObservableCollection to display them in the DataGrid
The user then changes a value in the DataGrid. When the cell loses focus it fires off a command that hooks the API and updates the parameter's value.
The change is made and the internal logic of the element calculates it's values based on the changed parameter
The calculated parameter values are changed in the model
The ViewModel checks each parameter to see if its value has changed, and updates any of the wrapped parameters in the ObservableCollection to reflect the changes.
The ObservableCollection fires off it's collection changed event to notify the DataGrid that values have changed
The DataGrid updates it's values to complete the process.
The issue currently lies in the very last step. Once the collection changed event is complete the wrapped parameter value matches the parameter value from the API, but the DataGrid will not redraw the information. Once you minimize the window, click into the cell, or scroll the DataGrid to where the cell is not visible the cell will show the new value when it comes back into view.
I can't seem to find a way to keep with MVVM principles and force the cells to redraw with their updated value. Am I missing something with this? How do I get the DataGrid to update without having to completely clear and reset the ObservableCollection items?
Things I have Tried
I had to create a custom ObservableCollection to implement INPC for the items in the collection, and from debugging it appears to work as intended. Each time an item in the ObservableCollection is updated it makes the change subscribes it to INPC and raises the collection changed event.
For each of the columns I have the binding set to Mode="TwoWay" and have tried setting the UpdateSourceTrigger="PropertyChanged", and neither helped.
I originally was using a <ContentPresenter/> in a <DataGridTemplteColumn/> to present different cell types, but even using a basic <DataGridTextColumn doesn't work.
---- CODE ----
XAML:
<DataGrid Grid.Row="2" ItemsSource="{Binding TestingParameters, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn IsReadOnly="True" Binding="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Name"/>
<DataGridTextColumn IsReadOnly="False" Binding="{Binding Path=ParamValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Param Value">
<i:Interaction.Triggers>
<i:EventTrigger EventName="LostFocus">
<i:InvokeCommandAction Command="{Binding UpdateParametersCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
C# ViewModel
public class ViewModelParameterPane : ViewModelBase
{
private ExternalEvent _event;
private HandlerED _handler;
private UIApplication _uiapp;
private UIDocument _uidoc;
private Document _doc;
private TestObservableCollection<WrappedParameter> _testParameter = new TestObservableCollection<WrappedParameter>();
public TestObservableCollection<WrappedParameter> TestParameter
{
get => _testParameter;
set
{
_testParameter = value;
RaiseProperty(nameof(_testParameter));
}
}
public ViewModelParameterPane(ExternalEvent exEvent, HandlerED handler, UIApplication uiapp)
{
_event = exEvent;
_handler = handler;
_uiapp = uiapp;
_uidoc = _uiapp.ActiveUIDocument;
_doc = _uidoc.Document;
_testParameter.ItemPropertyChanged += _testParameter_ItemPropertyChanged;
_testParameter.CollectionChanged += _testParameter_CollectionChanged;
UpdateParametersCommand = new RelayCommand(CallUpdateParameters);
}
private void _testParameter_ItemPropertyChanged(object sender, ItemPropertyChangedEventArgs<WrappedParameter> e)
{
Debug.WriteLine("PROPERTY CHANGE");
RaiseProperty(nameof(TestParameter));
int index = TestParameter.IndexOf(e.Item);
TestParameter[index] = e.Item;
}
private void _testParameter_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
Debug.WriteLine("COLLECTION CHANGE");
}
private void MakeRequest(RequestIdED request)
{
_handler.Request.Make(request);
_event.Raise();
}
private void CallUpdateParameters() { MakeRequest(RequestIdED.UpdateParameters); }
public void UpdateParameters()
{
Debug.WriteLine("Running Update Parameters");
try
{
using (var transaction = new Transaction(_doc))
{
transaction.Start("T_UpdateParameters");
foreach (WrappedParameter p in TestParameter)
{
string currenValue = p.RevitParameter.AsValueString();
if (p.RevitParameter.AsValueString() != p.ParamValue)
{
bool setValueSuccess = p.SetRevitParameterValue(p.ParamValue);
if(!setValueSuccess)
{
TaskDialog.Show("Parameter Value Not Set", "The parameter value for the parameter " + p.Name + " was not given a valid value and was not changed. Please ensure the units are correct.");
}
}
}
transaction.Commit();
}
}
catch (Exception e)
{
throw new Exception("Something Went Wrong. Check your values.");
}
}
public void UpdateParameterValues()
{
for(var i = 0; i < TestParameter.Count; i++)
{
TestParameter[i].UpdateValues();
}
}
}
C# Parameter Wrapper Class
public class TestParameter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(nameof(_name));
}
}
private string _categories;
public string Categories
{
get => _categories;
set
{
_categories = value;
OnPropertyChanged(nameof(_categories));
}
}
private string _paramValue;
public string ParamValue
{
get => _paramValue;
set
{
_paramValue = value;
OnPropertyChanged(nameof(_paramValue));
}
}
private Parameter _revitParameter;
public Parameter RevitParameter
{
get => _revitParameter;
set
{
_revitParameter = value;
OnPropertyChanged(nameof(_revitParameter));
}
}
private ElementId _elementId;
public ElementId ElementId
{
get => _elementId;
set
{
_elementId = value;
OnPropertyChanged(nameof(_elementId));
}
}
public TestParameter(Parameter param)
{
GetRevitParameterValue();
}
public void GetRevitParameterValue()
{
//Get parameter value logic
}
public bool SetRevitParameterValue(string Value)
{
//Set parameter value logic
}
}
C# TestObservableCollection Class
public class TestObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
public event EventHandler<ItemPropertyChangedEventArgs<T>> ItemPropertyChanged;
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
item.PropertyChanged += item_PropertyChanged;
}
protected override void RemoveItem(int index)
{
var item = this[index];
base.RemoveItem(index);
item.PropertyChanged -= item_PropertyChanged;
}
protected override void ClearItems()
{
foreach (var item in this)
{
item.PropertyChanged -= item_PropertyChanged;
}
base.ClearItems();
}
protected override void SetItem(int index, T item)
{
var oldItem = this[index];
oldItem.PropertyChanged -= item_PropertyChanged;
base.SetItem(index, item);
item.PropertyChanged += item_PropertyChanged;
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnItemPropertyChanged((T)sender, e.PropertyName);
}
private void OnItemPropertyChanged(T item, string propertyName)
{
var handler = this.ItemPropertyChanged;
if (handler != null)
{
handler(this, new ItemPropertyChangedEventArgs<T>(item, propertyName));
}
}
}
public sealed class ItemPropertyChangedEventArgs<T> : EventArgs
{
private readonly T _item;
private readonly string _propertyName;
public ItemPropertyChangedEventArgs(T item, string propertyName)
{
_item = item;
_propertyName = propertyName;
}
public T Item
{
get { return _item; }
}
public string PropertyName
{
get { return _propertyName; }
}
}

Binding An Observable Collection of Class Objects To a ListBox and UserControl (WPF)

What I'm trying to do:
I'm binding a ListBox called "LBShortcuts" to a class called "ShortcutList", that both inherits ObservableCollection<T> and holds objects of the class Shortcut. I bind it by setting the ListBox's DataContext within the MainWindow's constructor. Then I have a UserControl called UCShortcut as a DataTemplate which binds a property within the Shortcut object. When I click a button on the main window, it should add a shortcut to the ObservableCollection, which should invoke the CollectionChanged event, which then should alert the data-bound ListBox to update its list, and then all of its items.
What is happening:
Absolutely nothing. When I try to add shortcuts to the list via a button on the window, it adds them to the list, but nothing shows up. I've even registered the MainWindow's object to the ObservableCollection.CollectionChanged event, but it seems like it doesn't fire, even when I explicitly invoke it. Because of that, nothing shows up in the list.
What I've tried:
I tried to create a custom list class using the INotifyPropertyChanged and INotifyCollectionChanged interfaces.
I tried to debug the binding process using System.Diagnostics.PresentationTraceLevel, and Setting it in the Immediate window. Can't remember where I went wrong, but it didn't work.
I tried to change my debug options to show me everything about binding in the WPF application, but all it did was show me worrisome binding failures I had nothing to do with:
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:Path=IsInAppMenuExpanded; DataItem=null; target element is 'StackPanel' (Name='ButtonStackPanel'); target property is 'Visibility' (type 'Visibility')
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:Path=ShowLiveVisualTreeCommand; DataItem=null; target element is 'Button' (Name='ShowLiveVisualTree'); target property is 'Command' (type 'ICommand')
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:Path=IsInAppSelectionEnabled; DataItem=null; target element is 'ToggleButton' (Name='InAppSelection'); target property is 'IsChecked' (type 'Nullable`1')
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:Path=AreLayoutAdornersEnabled; DataItem=null; target element is 'ToggleButton' (Name='LayoutAdorners'); target property is 'IsChecked' (type 'Nullable`1')
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:Path=IsTrackFocusedElementEnabled; DataItem=null; target element is 'ToggleButton' (Name='TrackFocusedElement'); target property is 'IsChecked' (type 'Nullable`1')
...I don't even have Toggle buttons in this application.
I tried to isolate the binding by removing the DataTempate.
I tried changing the UCShortcut's TextBlock.Text binding to {Binding BoundShortcut.Name, RelativeSource={RelativeSource AncestorType=UserControl}}
I tried again using VS' databinding dialog box to get around the same idea: {Binding BoundShortcut.Name, ElementName=userControl, FallbackValue=Binding Invalid}
I'm really not sure what I'm doing wrong here. Based on everything I've read about this, this should be working. However...here I am. Help, please?
Details:
ListBox (LBShortcuts):
<Window xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase">
.
.
.
<ListBox x:Name="LBShortcuts" ItemsSource="{Binding}"
Grid.Row="1" BorderBrush="{x:Null}" Background="{x:Null}" IsManipulationEnabled="True" AllowDrop="True"
diag:PresentationTraceSources.TraceLevel="High">
<ListBox.ItemTemplate>
<DataTemplate>
<local:UCShortcut BoundShortcut="{Binding Shortcut}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
ListBox code-behind:
public MainWindow()
{
InitializeComponent();
Shortcuts = new ShortcutList();
Shortcuts.CollectionChanged += SaveList;
LBShortcuts.DataContext = Shortcuts;
SavingPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) +
"\\FolderShortcuts.json";
OpenList();
}
ShortcutList class:
public class ShortcutList : ObservableCollection<Shortcut>
{
public ShortcutList() : base()
{
shortcuts = new List<Shortcut>();
}
public ShortcutList(List<Shortcut> loadedList) : base()
{
shortcuts = new List<Shortcut>();
foreach (Shortcut sc in loadedList)
{
Add(new Shortcut(this,sc.Directory,sc.Name));
}
}
public void AddDirectory(string directory, string name)
{
DirectoryInfo di = new DirectoryInfo(directory);
Shortcut s = new Shortcut(this, di);
if (shortcuts.Where(x => x.Directory == di.FullName).Count() == 0)
{
Add(s);
}
}
}
Shortcut class:
[Serializable]
public class Shortcut : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string Directory { get; set; }
private string name;
//This must be a property before you can use it as a data binding path in the properties menu of the design page
public string Name
{
get {return name; }
set
{
if (name != value)
{
name = value;
OnPropertyChanged();
}
}
}
[NonSerialized]
public ShortcutList Parent;
private void OnPropertyChanged([CallerMemberName] string member_name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(member_name));
}
public Shortcut(){ }
public Shortcut(ShortcutList parent, DirectoryInfo directory)
{
Parent = parent;
Directory = directory.FullName;
Name = directory.Name;
}
public Shortcut(ShortcutList parent, string directory,string name)
{
Parent = parent;
Directory = new DirectoryInfo(directory).FullName;
Name = name;
}
}
UCShortCut (UserControl)
XAML:
<Grid VerticalAlignment="Center">
...
<TextBlock x:Name="TBkName" Text="{Binding Name, FallbackValue=Binding Invalid}"
Padding="4" Foreground="{DynamicResource ForegroundColor}"/>
...
</Grid>
code-behind:
public partial class UCShortcut : UserControl
{
public static readonly DependencyProperty ShortcutProperty;
public Shortcut BoundShortcut
{
get{ return (Shortcut)GetValue(ShortcutProperty); }
set{ SetValue(ShortcutProperty, value); }
}
static UCShortcut()
{
ShortcutProperty = DependencyProperty.Register("BoundShortcut",
typeof(Shortcut), typeof(UCShortcut));
}
public UCShortcut()
{
InitializeComponent();
}
private void RenameShortcut(object sender, RoutedEventArgs e)
{
TBxName.Text = TBkName.Text;
TBkName.Visibility = Visibility.Collapsed;
TBxName.Visibility = Visibility.Visible;
TBxName.Focus();
TBxName.SelectAll();
}
private void ChangeName(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter || e.Key == Key.Escape)
{
if (e.Key == Key.Enter)
{
BoundShortcut.Name = TBxName.Text;
}
TBkName.Visibility = Visibility.Visible;
TBxName.Visibility = Visibility.Collapsed;
TBxName.Text = string.Empty;
}
}
private void InitBoundShortcut(object sender, RoutedEventArgs e)
{
BoundShortcut = (Shortcut)DataContext;
}
}

WPF OneWay binding works only once

I have a View that have two comboboxes. One is where user selects routing pipe type name, and the other where there should be a list of available diameters for the chosen pipe type.
Whenever user selects the pipe type, the other combobox should update the list of available diameters.
AvailableDiameters and RoutingPipeTypeName properties are static in Context class, that implements INotifyPropertyChanged interface. In xaml I've set the bindings to these properties, in code behind also the DataContext.
The problem is that the list of diameters get's updated only once, when the view is initialized.
When debugging I can see that the properties backing field's values are updated properly when selection on pipe type name is changed, only in the UI the available diameters list is not updated...
Context class:
public class Context : INotifyPropertyChanged
{
public static Context This { get; set; } = new Context();
public static string RoutingPipeTypeName
{
get => _routingPipeTypeName;
set
{
if (_routingPipeTypeName != value)
{
_routingPipeTypeName = value;
This.OnPropertyChanged(nameof(RoutingPipeTypeName));
}
}
}
public static List<double> AvailableDiameters
{
get => _availableDiameters;
set
{
//check if new list's elements are not equal
if (!value.All(_availableDiameters.Contains))
{
_availableDiameters = value;
This.OnPropertyChanged(nameof(AvailableDiameters));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
xaml:
<ComboBox Width="80" SelectedValue="{Binding Path=RoutingPipeTypeName, Mode=OneWayToSource}">
<ComboBoxItem Content="Example pipe type 1"></ComboBoxItem>
<ComboBoxItem Content="Example pipe type 2"></ComboBoxItem>
</ComboBox>
<ComboBox Width="80" SelectedValue="{Binding Path=RoutingDiameter, Mode=OneWayToSource}" ItemsSource="{Binding Path=AvailableDiameters, Mode=OneWay}">
</ComboBox>
code behind:
public Context AppContext => Context.This;
public MyView()
{
InitializeComponent();
Instance = this;
DataContext = AppContext;
}
And the client class that is responsible for updating the list of diameters:
public void InitializeUIContext()
{
Context.This.PropertyChanged += UIContextChanged;
if (Cache.CachedPipeTypes.Count > 0)
Context.RoutingPipeTypeName = Cache.CachedPipeTypes.First().Key;
}
private void UIContextChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Context.RoutingPipeTypeName))
{
Context.AvailableDiameters = Cache.CachedPipeTypes.First().Value.GetAvailableDiameters();
}
}
I expected such set-up would update the diameters combobox each time the selection is changed on the pipe types property.
Instead it updates it only once, when the view is initialized... Why?
Do not use static properties for binding to an object (which you have correctly passed to the DataContext of your view).
Declare the properties without the static modifier and replace This.OnPropertyChanged by OnPropertyChanged:
public string RoutingPipeTypeName
{
get => _routingPipeTypeName;
set
{
if (_routingPipeTypeName != value)
{
_routingPipeTypeName = value;
OnPropertyChanged(nameof(RoutingPipeTypeName));
}
}
}
You should also remove the static This from your Context class and simply write
public Context AppContext { get; } = new Context();

Set the SelectionChanged event of a ComboBox while binding its SelectedItem and ItemsSource in XAML

I'm trying to set up a ComboBox with its options binded from a list of strings, its default selected value binded from a setting, and with an event handler for its selection changed.
I want to configure it all using XAML like so:
<ComboBox Name="RoutesComboBox"
ItemsSource="{Binding Routes}"
SelectedItem="{Binding DefaultRoute}"
SelectionChanged="RouteFilter_SelectionChanged" />
But when I do that on startup it throws the error:
An unhandled exception of type
'System.Reflection.TargetInvocationException' occurred in
PresentationFramework.dll
If I only do some of it in XAML, then either set the SelectionChanged event or the ItemsSource programatically in C# like below it works fine. But I have a lot of these ComboBoxes so I would rather do it straight in the XAML.
<ComboBox Name="RoutesComboBox"
ItemsSource="{Binding Routes}"
SelectedItem="{Binding DefaultRoute}" />
With this C#:
public IEnumerable<string> Routes
{
get { return LubricationDatabase.GetRoutes(); }
}
public string DefaultRoute
{
get { return MySettings.Default.DefaultRoute; }
set { } /* side question: without this, it throws a parse exception. Any idea why? */
}
public MainWindow()
{
this.DataContext = this;
InitializeComponent();
RoutesComboBox.SelectionChanged += RouteFilter_SelectionChanged;
}
I've also tried the solution found here:
private string _defaultRoute;
public string DefaultRoute
{
get { return MySettings.Default.DefaultRoute; }
set
{
if (_defaultRoute != value)
{
_defaultRoute = value;
// this fires before `SelectedValue` has been
// updated, and the handler function uses that,
// so I manually set it here.
RoutesComboBox.SelectedValue = value;
SelectionChangedHandler();
}
}
}
Which is okay, but is pretty bulky and probably more work than is worth it when I can just programatically assign the SelectionChanged event.
Again if possible I'd like to do it all using XAML because I have a lot of these ComboBoxes and initializing them all like this in the C# will look awful.
Any ideas?
Why are you binding with SelectedItem when you're not going to update the item when a user changes their selection? Not sure what your event handler is doing, but I have a working solution just the way you wanted it.
In short, you need to keep track of the DefaultRoute using a backing field. Also, you need to notify the UI when the selected item changes in your view model; which by the way is something you don't seem to be doing, MVVM. You should only be hooking into the selection changed event if you plan on updating the view in some way. All other changes should be handled in your view models DefaultRoute setter
XAML
<ComboBox Name="RoutesComboBox"
ItemsSource="{Binding Routes}"
SelectedItem="{Binding DefaultRoute}"
SelectionChanged="RouteFilter_SelectionChanged" />
Code
public partial class MainWindow : Window, INotifyPropertyChanged
{
public IEnumerable<string> Routes
{
get
{
return new string[] { "a", "b", "c", "d" };
}
}
public string DefaultRoute
{
get
{
return _defaultRoute;
}
set
{
_defaultRoute = value;
// Handle saving/storing setting here, when selection has changed
//MySettings.Default.DefaultRoute = value;
NotifyPropertyChanged();
}
}
public MainWindow()
{
this.DataContext = this;
InitializeComponent();
DefaultRoute = MySettings.Default.DefaultRoute;
}
private string _defaultRoute;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void RouteFilter_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
}
public static class MySettings
{
public static class Default
{
public static string DefaultRoute = "a";
}
}

Textbox.Text input not binding to Property

I have an object that is created, and want to bind to a property of that object through the mode OneWayToSource explicitly. This binding however is not working at all. It also has a red border around the textbox right when the program is initialized, when I only want the input validated once I click the button. My last ditch effor was embedding the source to the element itself, but no such luck. Here is what I have:
<StackPanel.Resources>
<my:HoursWorked x:Key="hwViewSource" />
</StackPanel.Resources>
<TextBox Style="{StaticResource textBoundStyle}" Name="adminTimeTxtBox">
<Binding Source="{StaticResource hwViewSource}" Path="Hours" UpdateSourceTrigger="PropertyChanged" Mode="OneWayToSource">
<Binding.ValidationRules>
<my:NumberValidationRule ErrorMessage="Please enter a number in hours." />
</Binding.ValidationRules>
</Binding>
</TextBox>
The HoursWorked object looks like this:
//I have omitted a lot of things so it's more readable
public class HoursWorked : INotifyPropertyChanged
{
private double hours;
public HoursWorked()
{
hours = 0;
}
public double Hours
{
get { return hours; }
set
{
if (Hours != value)
{
hours = value;
OnPropertyChanged("Hours");
}
}
}
#region Databinding
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(String info)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
once the window is initialized, this is the portion of code I have:
public partial class Blah : Window
{
private HoursWorked newLog;
public Blah()
{
InitializeComponent();
newLog = new HoursWorked();
adminTimeTxtBox.DataContext = newLog;
}
private void addAdBtn_Click(object sender, RoutedEventArgs e)
{
AddHours();
}
private void AddHours()
{
if (emp.UserType.Name == "Admin")
{
if(!ValidateElement.HasError(adminTimeTxtBox))
{
item.TimeLog.Add(newLog);
UpdateTotals();
adminTimeTxtBox.Clear();
}
}
}
}
and finally ValidateElement looks like this:
public static class ValidateElement
{
public static bool HasError(DependencyObject node)
{
bool result = false;
if (node is TextBox)
{
TextBox item = node as TextBox;
BindingExpression be = item.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
}
if (Validation.GetHasError(node))
{
// If the dependency object is invalid, and it can receive the focus,
// set the focus
if (node is IInputElement) Keyboard.Focus((IInputElement)node);
result = true;
}
return result;
}
}
It validates properly, but every time I check to see if the property updates, it doesn't. I really need help on this, any help would be greatly appreciated.
You've got 2 instances of the HoursWorked class.
One is created in Resources via this tag <my:HoursWorked x:Key="hwViewSource" /> but then you create one in your Window with newLog = new HoursWorked(); and set it into the DataContext of the adminTimeTxtBox...so the one your Binding to (the Resource one) isn't the same as the one you're updating (the one inside Window).
You could change the Binding to
<Binding Source="{Binding}" ....
and then don't need the one defined in Resource.
TextBox.Text property is of type string, your Hours property is double.
You have to create a ValueConverter or an auxiliary property for parsing the string to double and vice versa.

Categories