MVVM Binding to Textbox: Need to call GET after SET somehow - c#

I am attempting to implement some simple validation on a textbox in MVVM
public string Property
{
get
{
if (App.PropertyStorageContainer != null)
{
return App.PropertyStorageContainer.Property;
}
else
{
return null;
}
}
set
{
App.PropertyStorageContainer.Property = value;
RaisePropertyChanged("Property");
}
}
Then in my PropertyStorageContainer class I have
private string _property;
public string Property
{
get
{
return App.PropertyStorageContainer.Property;
}
set
{
if(value meets some condition)
{
_property = value;
}
else
{
_property = someothervalue;
}
}
}
.
<TextBox Width="50" TextAlignment="Center" Text="{Binding Property, Mode=TwoWay, NotifyOnValidationError=True}" MaxLength="3"></TextBox>
The point of this is to validate what goes in the box. Now if I set this value directly from my code then everything works as I would expect. It attempts to SET the value, then calls RaiseProperyChanged, then GET the value (which because of the validation may not be the same value that was entered originally). The final value retrieved does show up on the view, so I know TwoWay binding is working.
The problem I have is when the input for SET comes from the bound XAML property / directy from user. In this case the SET method is called, the validation performed, but the GET never happens. This results in the unvalidated value remaining in the textbox on screen.
My first question would be is this a bug or expected behavior? I can see how maybe they tried to save performance by removing that last GET when the input came straight from the user since there should be nothing new to GET. But if not then maybe the way I have it all setup is interfering with the GET being called.
Second question is of course any suggestions for getting around this one. I've read a few suggestions for other methods of doing validation, but my program is already live on PROD and most of the changes being suggested involve a lot of rework for me so I am hoping to find a way to make it call GET any time the property is SET.

I have made a couple of assumptions since I am not sure I understand you code completely but I think you could consider possibly implementing a custom validation rule. First off, since your custom ValidationRule will take care of the validation you could get the logic out of your model class's property definition and "dumb down" your poco:
class PropertyStorageContainer
{
public string Property { get; set; }
}
It seems you desire your view model to act as a basic wrapper around your model class. Again, I will assume this is valid based on the description of your scenario:
class PropertyStorageContainerViewModel : INotifyPropertyChanged
{
private PropertyStorageContainer model;
public PropertyStorageContainerViewModel(PropertyStorageContainer model)
{
this.model = model;
}
public string Property
{
get
{
if (model != null)
{
return model.Property;
}
else
{
return null;
}
}
set
{
if (model.Property != value)
{
model.Property = value;
RaisePropertyChanged("Property");
}
}
}
// INotifyPropertyChanged implementation...
}
Now create a new class that extends System.Windows.Controls.ValidationRule and override the abstract Validate method in order implement your validation logic. For the example, I created a rule that just checks if the string is null or empty (assuming that would be an invalid scenario):
class IsNullOrEmptyValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
string s = (value ?? string.Empty).ToString();
if (string.IsNullOrEmpty(s))
{
// Invalid...
return new ValidationResult(false, "Please enter a value.");
}
else
{
// Valid...
return new ValidationResult(true, null);
}
}
}
Now for the XAML... Here is an example of a TextBox that adds the validation rule to its binding validation rules (can be multiple rules).
<TextBox Name="textBox1" Width="50" FontSize="12"
Validation.ErrorTemplate="{StaticResource validationTemplate}"
Style="{StaticResource textBoxInError}">
<TextBox.Text>
<Binding Path="Property" UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<local:IsNullOrEmptyValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Then define the following resources (referenced above) somewhere (e.g., Window.Resources). First a ControlTemplate to define how the TextBox should look when in invalid state:
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<TextBlock Foreground="Red" FontSize="15" Text="!!!" />
<AdornedElementPlaceholder/>
</DockPanel>
</ControlTemplate>
Additionally you could define a style trigger to display the error message. Here I just bind it to the ToolTip property of the TextBox:
<Style x:Key="textBoxInError" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>

You're going into INPC hell right now. I've been there and it's not fun.
That's a big no-no, especially since if any mapping is done on such classes, those getters and setters will be called outside of their WPF binding context and hell breaks lose.
Keep it simple: bind straight to App.PropertyStorageContainer.Property
For the second case, either:
Use data validation
Let the property be set not by binding but through a Command, in which you can do such value swap.
Do yourself a favor and don't abuse properties' get/set

Related

Validation based on existing data in WPF

I need to create a validation node that will return an error if value entered already exists. I have GUI with items that can have their name set. I want to enforce the names to be unique.
So for each validation, I need following two parameters:
List of all names of all items, or some predicate that will tell me a name exists
Current items name, to exclude it from the above validation (changing the name to the same value should not be an error)
The data contexts look like this (just the interface for illustration):
class AppMainContext
{
public IEnumerable<string> ItemNames {get;}
public Item SelectedItem {get;}
}
class Item
{
public string Name {get;}
}
The field in WPF looks like this and its parent is bound to `{SelectedItem}:
<DockPanel DockPanel.Dock="Top">
<Label Content="Name: "/>
<TextBox DockPanel.Dock="Top">
<TextBox.Text>
<Binding Path="Name" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<vmvalidation:UniqueNameRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</DockPanel>
The validator looks like this:
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Controls;
namespace MyApp.Validation
{
public class UniqueNameRule : ValidationRule
{
public IEnumerable<string> ExistingNames { get; set; }
public string MyName { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if(value is string newValue)
{
// name changed
if(!value.Equals(MyName))
{
if(ExistingNames.Contains(newValue))
{
return new ValidationResult(false, "Name already exists!");
}
}
return new ValidationResult(true, null);
}
else
{
return new ValidationResult(false, "Invalid value type. Is this validator valid for the given field?");
}
}
}
}
I tried to at least bind current name to the validator. The text box already exists in current items data context, so a correct binding would be:
<Binding.ValidationRules>
<vmvalidation:UniqueNameRule MyName="{Binding Name}" />
</Binding.ValidationRules>
Except that this gives an error:
The member MyName is not recognized or is not accessible.
The list of all items is in the windows data context, accessible through ItemNames. I suppose it could be accessed like this:
{Binding Path=DataContext.ItemNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}
I tried correct binding using an answer below, but I then get an error:
A 'Binding' cannot be set on the 'MyName' property of type MyProject_Validation_UniqueNameRule_9_468654. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
Looks like bindings are not supported at all.
So how can I put this together, so that the validation rule can access both of these variables?
The binding is failing due to the nature of how the validation rule falls on the visual tree, and maybe is what you suspect.
There are other flavors of RelativeSource (see the properties section in that document) on bindings.
Ultimately one wants the parent node, here is one used on styles which might be relevant:
<vmvalidation:UniqueNameRule
MyName="{Binding Name, RelativeSource={RelativeSource TemplatedParent}}"/>
Or work your way up the chain, instead of x:Type Window how the more likely binding to the parent such as x:Type TextBox:
<vmvalidation:UniqueNameRule
MyName="{Binding Name, RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type TextBox}}"/>

Control Content and Property change

I have dialogbox with Content control with templates:
<ContentControl Content="{Binding Model,UpdateSourceTrigger=PropertyChanged}" ContentTemplateSelector="{StaticResource TemplateSelector}"/>
and property change event at dialogbox context:
dialogContext.Model.PropertyChanged += (s, e) => Change(s,e, context);
private void Change(object s, PrropertyChangeEventArgs e, Context context)
{
...
context.Mode = new Example()
{
...
}
model.PropertyChanged += (sender, eventArgs) =>
ModelChange(sender, eventArgs, context);
context.Model = model;
}
I want to change some properties at model, that determine which custom template will be displayed.
To reload new template and invoke temlate selector should I create new model and
add property change event to this. Is is ok, or is it another way to do this.
Update
The below implementation doesn't work because it turns out that the template selector is only reinvoked if the actual value of ContentControl.Content changes. If you've still got the same instance of Model, raising PropertyChanged will have no effect. I even tried overriding ModelClass.Equals() and ModelClass.GetHashCode(). Neither was called. Maybe the Binding is calling Object.ReferenceEquals().
But I did find three ways to do this. All have been tested, now that I've learned my lesson.
If you're going to this much trouble to get a template selector to work, best to look for some other approach where you're not fighting the framework.
You could instead use style triggers to swap templates:
<ContentControl
Content="{Binding Model}"
>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Model.Foo}" Value="foo">
<Setter
Property="ContentTemplate"
Value="{StaticResource Foo}"
/>
</DataTrigger>
<DataTrigger Binding="{Binding Model.Foo}" Value="bar">
<Setter
Property="ContentTemplate"
Value="{StaticResource Bar}"
/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
...but the logic in your template selector may be quite a bit more complicated than that, in which case it may not be feasible.
Here's another. You don't need a template selector to select a template. A converter can return a DataTemplate too, and if you use a multi-binding converter, you can give it whatever it needs to look up a DataTemplate in the resources:
<ContentControl
Content="{Binding Model}"
>
<ContentControl.ContentTemplate>
<MultiBinding
Converter="{StaticResource ContentTemplateConverter}"
>
<!--
We must bind to Model.Foo so the binding updates when that changes,
but we could also bind to Model as well if the converter wants to
look at other properties besides Foo.
-->
<Binding Path="Model.Foo" />
<!-- The ContentControl itself will be used for FindResource() -->
<Binding RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</ContentControl.ContentTemplate>
</ContentControl>
C#
public class ContentTemplateConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var ctl = values[1] as FrameworkElement;
switch ($"{values[0]}")
{
case "foo":
return ctl.FindResource("Foo") as DataTemplate;
case "bar":
return ctl.FindResource("Bar") as DataTemplate;
}
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
One last possibility, and in my opinion the least, is to use the template selector, but make it work by actually replacing the value of Model every time one of its properties changes. Rewrite ModelClass so it can easily be cloned:
public ModelClass() {}
public ModelClass(ModelClass cloneMe) {
this.Foo = cloneMe.Foo;
this.Bar = cloneMe.Bar;
}
...and keep _model_PropertyChanged from my original answer, but change the guts so instead of merely raising PropertyChanged, it replaces the actual value of Model (which will of course still raise PropertyChanged, as a side effect):
private void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ModelClass.Foo))
{
Model = new ModelClass(Model);
}
}
I've tested that and while it's alarmingly goofy, it does work.
Instead of cloning ModelClass, you could use a "reference" class for the parent's Model property:
public class ModelClassRef {
public ModelClassRef(ModelClass mc) { ... }
public ModelClassRef { get; private set; }
}
But it's still wicked goofy. The viewmodel shouldn't "know" the view even exists, but here you are rewriting a chunk of it in a bizarre way just to work around a peculiarity in the implementation of a particular control. View workarounds belong in the view.
So when this.Model.Foo changes, you want to change the template? I would expect this to do the job:
#region Model Property
private ModelClass _model = null;
public ModelClass Model
{
get { return _model; }
set
{
if (value != _model)
{
if (_model != null)
{
_model.PropertyChanged -= _model_PropertyChanged;
}
_model = value;
if (_model != null)
{
_model.PropertyChanged += _model_PropertyChanged;
}
OnPropertyChanged();
}
}
}
private void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// If Model.Foo changed, announce that Model changed. Any binding using
// the Model property as its source will update, and that will cause
// the template selector to be re-invoked.
if (e.PropertyName == nameof(ModelClass.Foo))
{
OnPropertyChanged(nameof(Model));
}
}
This is defined in your viewmodel base class. Maybe you've already got essentially the same method and it's called something else; if so, use that one of course.
protected void OnPropertyChanged([CallerMemberName] String propName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
By the way, get rid of UpdateSourceTrigger=PropertyChanged. ContentControl will never create a new value for its Content property and pass that back to your viewmodel through the binding. Can't, won't, and you wouldn't want it to. So you don't need to tell it exactly when to perform a task it's not capable of performing.

Change the visibility of elements in ComboBox dynamically using MVVM framework

Ok,
I have seen a few similar questions but have not been able to figure out this problem for the past couple days. I have two Comboboxes and I want each one to hide the selected element in the other one. For example, if I select a value in ComboBox 1 that selected item should be removed as an option in ComboBox 2.
I thought about using a command but ComboBoxes don't have commands. I have pasted below the comboboxes' XAML and ViewModel code. I would appreciate any help with this. I know the code below is wrong but I think that the logic for this should be in the setters of the bounded to ItemSource.
<ComboBox Margin="0,7,0,0"
Name="ComboBoxA"
HorizontalAlignment="Stretch"
Header="{Binding AccountHeader}"
ItemTemplate="{StaticResource ComboBoxTemplate}"
ItemsSource="{Binding ChargedAccounts,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedAccount,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Uid="TargetAccountTextBox"
Name="ComboBoxB"
Margin="0,7,0,0"
HorizontalAlignment="Stretch"
Header="target account"
ItemTemplate="{StaticResource ComboBoxTemplate}"
ItemsSource="{Binding TargetAccounts,
Mode=TwoWay,
namespace MoneyFox.Shared.ViewModels
{
[ImplementPropertyChanged]
public class ModifyPaymentViewModel : BaseViewModel, IDisposable
{
private readonly IDefaultManager defaultManager;
private readonly IDialogService dialogService;
private readonly IPaymentManager paymentManager;
//this token ensures that we will be notified when a message is sent.
private readonly MvxSubscriptionToken token;
private readonly IUnitOfWork unitOfWork;
// This has to be static in order to keep the value even if you leave the page to select a category.
private double amount;
private Payment selectedPayment;
public ModifyPaymentViewModel(IUnitOfWork unitOfWork,
IDialogService dialogService,
IPaymentManager paymentManager,
IDefaultManager defaultManager)
{
this.unitOfWork = unitOfWork;
this.dialogService = dialogService;
this.paymentManager = paymentManager;
this.defaultManager = defaultManager;
TargetAccounts = unitOfWork.AccountRepository.Data;
ChargedAccounts = unitOfWork.AccountRepository.Data;
token = MessageHub.Subscribe<CategorySelectedMessage>(ReceiveMessage);
}
ObservableCollection<Account> _SelectedAccount;
ObservableCollection<Account> SelectedAccount
{
get
{
return _SelectedAccount;
}
set
{
_SelectedAccount = value;
for(int i = 0; i < ChargedAccounts.Count; i++)
{
if(ChargedAccounts[i].ToString() == _SelectedAccount.ToString())
{
ChargedAccounts.Remove(ChargedAccounts[i]);
}
}
}
}
ObservableCollection<Account> _TargetAccount;
ObservableCollection<Account> Targetccount
{
get
{
return _SelectedAccount;
}
set
{
_SelectedAccount = value;
for (int i = 0; i < TargetAccounts.Count; i++)
{
if (TargetAccounts[i].ToString() == _SelectedAccount.ToString())
{
TargetAccounts.Remove(ChargedAccounts[i]);
}
}
}
}
While I do agree with a lot of the points in the answer provided by Ed, there is a simpler way to do this without DataTriggers or Converters. There is already a filterable CollectionViewSource in the framework that is your friend (Scott Hanselman loves it)
I would bind ComboBoxA to your regular ChargedAccounts property, but I would modify ComboBoxB to:
bind to a property in the code behind of the View that returns a ICollectionView
in a SelectionChanged event handler for ComboBoxA (also in the code behind of the view) I would adjust the filter for the ICollectionView to exclude the currently selected item
Roughly, this can be done in just a couple of lines:
public ICollectionView FilteredData { get; set; }
private void ComboBoxA_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var z = new CollectionViewSource {Source = ViewModel.ChargedAccounts.Where(p => p != ViewModel.SelectedAccount) };
FilteredData = z.View;
}
Of course this assumes you've done the right thing by having a ViewModel property in the code behind of your view preferably exposed as an interface, and that the ChargedAccounts and SelectedAccount properties are available via that interface.
You could also cobble these couple of lines together in your viewmodel and trigger it via a property change on SelectedAccount - I just have the opinion that a filter operation in response to a UI action should go in the code behind of the UI, but that decision is really up to you.
Give the comboboxes an ItemContainerStyle (TargetType="ComboBoxItem") with a data trigger. For ComboBoxA, that'll look like this:
<ComboBox
...
x:Name="ComboBoxA"
...
>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding
Converter="{local:ObjectEquals}"
>
<Binding
Path="SelectedItem"
ElementName="ComboBoxB" />
<!-- Binding with no properties just binds to the DataContext -->
<Binding />
</MultiBinding>
</DataTrigger.Binding>
<Setter
Property="Visibility"
Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
ComboBoxB gets the same deal, but ElementName="ComboBoxA" in the SelectedItem binding.
And we'll need to write that multi-value converter. It's as easy as they come:
public class ObjectEquals : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Length == 2 && values[0] == values[1];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
It'd be so convenient if you could bind DataTrigger.Value to {Binding}, but it's not a dependency property.
You *could^ also do this purely in the viewmodel by temporarily removing SelectedAccount from TargetAccounts -- you'd have a private full _targetAccountsFull list, and a public filtered one. The setter for SelectedAccount would filter the list. Were you trying to do that already?
But that's not my idea of a good solution. Hiding combo box items is UI design stuff; the viewmodel shouldn't be involved in it, and in fact shouldn't even be aware that such things take place. One of the pleasures of WPF/MVVM is that you can separate that stuff out into pure UI code in the view. The viewmodel has its own complexities to worry about.
By the way, you bind SelectedItem to SelectedAccount, but SelectedAccount is an ObservableCollection. That makes no sense. There's one selected account. Make it a single Account, not a collection of them.

How do I use an ObjectDataProvider to set the value of text in a textbox from a method?

Background: I'm new to WPF and have been trying to teach myself for a couple weeks now. I have an app that runs in a NavigationWindow with a few pages. The page in question has 5 textboxes, 4 of which are backed with dependency properties. Three of them are set up with ValidationRules for times, the fourth has ValidationRules for type double. The fifth textbox is the output of the calculation made from a button click event. The button is bound to a MultiDataTrigger, which enables the button when there are no validation errors. Buddy says "hey you have everything bound already, why not update the output box on binding so you don't have to click a button?".
This seems like a good idea and a nice weapon to put in my wpf toolbox. The button serves two purposes, to calculate the time for the output textbox, and to offer to navigate to another page with the current values. If I could show the result of the calculation in the textbox with a binding, I would just use the button to navigate to the next page. I've tried setting up an ObjectDataProvider to use with the fifth textbox so I can call a method to populate the result with a binding. So far I've only succeeded in causing numerous errors, including causing a stackoverflow on the page call to InitializeComponent();
public static readonly DependencyProperty timeBoxProperty =
DependencyProperty.Register("timeBox", typeof(string),
typeof(TtlPage), new UIPropertyMetadata("07:30"));
public static readonly DependencyProperty timeBoxProperty2 =
DependencyProperty.Register("timeBox2", typeof(string),
typeof(TtlPage), new UIPropertyMetadata("13:00"));
public static readonly DependencyProperty timeBoxProperty3 =
DependencyProperty.Register("timeBox3", typeof(string),
typeof(TtlPage), new UIPropertyMetadata("13:40"));
public static readonly DependencyProperty hoursBoxProperty =
DependencyProperty.Register("hoursBox", typeof(string),
typeof(TtlPage), new UIPropertyMetadata("9.00"));
public string timeBox
{
get { return (string)GetValue(timeBoxProperty); }
set { SetValue(timeBoxProperty, value); }
}
public string timeBox2
{
get { return (string)GetValue(timeBoxProperty2); }
set { SetValue(timeBoxProperty2, value); }
}
public string timeBox3
{
get { return (string)GetValue(timeBoxProperty3); }
set { SetValue(timeBoxProperty3, value); }
}
public string hoursBox
{
get { return (string)GetValue(hoursBoxProperty); }
set { SetValue(hoursBoxProperty, value); }
}
Part of button click, given the above, should I be accessing the textbox.text like below using the Textbox.Name property, or should I be grabbing it from the property or DependencyProperty above?:
private void Button_Click(object sender, RoutedEventArgs e)
{
DateTime inTime = DateTime.Parse(ttlInTime.Text);
DateTime outLunch = DateTime.Parse(ttlOutLunch.Text);
DateTime inLunch = DateTime.Parse(ttlInLunch.Text);
decimal hours = decimal.Parse(ttlHours.Text);
//etc.
}
The method for the ObjectDataProvider:
public string UpdateOutput()
{
//do stuff
}
Some XAML ObjectDataProvider, one of the input textboxes, and the output textbox:
<ObjectDataProvider x:Key="outputBox" ObjectType="{x:Type sys:String}" MethodName="UpdateOutput"/>
<Style x:Key="timeBox3" TargetType="TextBox" BasedOn="{StaticResource tbStyle}">
<Setter Property="Text">
<Setter.Value>
<Binding ElementName="This" Path="timeBox3" UpdateSourceTrigger="
<Binding.ValidationRules>
<local:TimeValidation/>
</Binding.ValidationRules>
</Binding>
</Setter.Value>
</Setter>
</Style>
<TextBox Name="ttlInLunch" Style="{StaticResource timeBox3}" Grid.Row="2" Grid.Column="1" TextChanged="TimeBox_TextChanged"
GotFocus="TimeBox_GotFocus"/>
<TextBox Margin="0,2,2,1" Name="ttlOutput" Grid.Row="4" Grid.Column="1" IsReadOnly="True" Background="Transparent" IsTabStop="False"
Text="{Binding Source={StaticResource outputBox}}"/>
So, I've been here http://msdn.microsoft.com/en-us/library/aa348824(v=vs.110).aspx, and worked with the example, and after a while, realized that the ObjectType wasn't supposed to be the return type of the method. It was actually just the name of the containing class, so I used ttlPage as the type (which is the page itself ttlPage : Page), and caused a stack overflow. I've done a ton of Googling and haven't come up with anything helpful. I haven't created any sort of converter for it, because the method returns a string, which I would assume is suitable for the textbox.text property. I've set a breakpoint in the UpdateOutput method, and have found that it doesn't even get called. How do I call the UpdateOutput method and have it's result bound to the output textbox while the user is typing? As far as when I calculate, I was just going to return from the method until there are no validation errors, at which point I would perform my calculations and return the calculated value ToString();
Try changing the access modifier to public for your method UpdateOutput. Currently it's a private method, so can't be executed by the framework.
public string UpdateOutput()
{
//do stuff
}
Bartosz was correct, I needed to define another class to hold my UpdateOutput method. There were also several other factors which contributed to the frustration. First I created a class to hold the method. I then found out the hard way that I forgot a default constructor on said class. Additionally I found I was not able to use DependencyProperties as parameters for the ObjectDataProvider. I removed the entire set of DependencyProperties and their respective bindings. The styles referencing these were also removed, as were the bindings to the validation class.
//the containing class
public partial class AutoFillBox
{
public AutoFillBox()
{
//dont forget a default constructor
}
public string UpdateOutput(string time1, string time2, string time3, string time4)
{
//do stuff
}
}
The ObjectDataProvider:
<ObjectDataProvider ObjectType="{x:Type local:AutoFillBox}" MethodName="UpdateOutput" x:Key="odpOutput">
<ObjectDataProvider.MethodParameters>
<sys:String>08:00</sys:String>
<sys:String>12:00</sys:String>
<sys:String>13:00</sys:String>
<sys:String>18:00</sys:String>
</ObjectDataProvider.MethodParameters>
Then it was simply bind the appropriate textboxes to the MethodParameters:
<TextBox Name="recIn" Style="{StaticResource tbStyle}" Grid.Row="1" Grid.Column="1"
TextChanged="TimeBox_TextChanged" GotFocus="TimeBox_GotFocus">
<TextBox.Text>
<Binding Source="{StaticResource odpOutput}" Path="MethodParameters[0]" BindsDirectlyToSource="True" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:TimeValidation/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
And bind the output of the method to a textbox control:
<TextBox Margin="0,2,2,1" Name="recOutput" Grid.Row="5" Grid.Column="1" IsReadOnly="True" Background="Transparent" IsTabStop="False"
Text="{Binding Source={StaticResource odpOutput}, Mode=OneWay}"/>

Set style of extended TreeViewItem based on an extended property value

I have extended the TreeViewItem class to allow me to store extra data within a tree view item. I would like to be able to set the style of the treeview item based on the value of one of the extended properties I have added.
So far I have:
namespace GX3GUIControls
{
public class GX3TreeViewItem : TreeViewItem
{
public bool Archived { get; set; }
public object Value { get; set; }
}
}
<src:GX3ClientPlugin.Resources>
<Style TargetType="{x:Type Controls:GX3TreeViewItem}">
<Style.Triggers>
<DataTrigger Archived="True">
<Setter Property="Background" Value="Gray" />
<Setter Property="FontStyle" Value="Italic" />
</DataTrigger>
</Style.Triggers>
</Style>
</src:GX3ClientPlugin.Resources>
But I get the error - Error 1 The property 'Archived' was not found in type 'DataTrigger
DataTrigger has no Archived property, but you can bind your Achived-property to it via the Binding property like so <DataTrigger Binding="{Binding Path=Archived}" Value="True">
To notify your view if the Achived property changes, you could either:
1.Implement the INotifyPropertyChanged Interface in your GX3TreeViewItem-class: public class GX3TreeViewItem : TreeViewItem, INotifyPropertyChanged, create a method which raises the PropertyChanged Event:
private void PropertyChanged(string prop)
{
if( PropertyChanged != null )
{
PropertyChanged(this, new PropertyChangedEventArgs(prop);
}
}
and place this method in the setter of your property:
private bool _achived;
public bool Achived
{
get
{
return _achived;
}
set
{
_achived = value;
PropertyChanged("Achived");
}
}
2.Or make your property a DependencyProperty.
Honestly it seems like you're doing it wrong. Those properties should be on your data.
You can do something like this,
Style="{Binding Path=Archived, Converter={StaticResource GetStyle}}"
GetStyle is an IValueConverter, no need to extend TreeView imo.
This is not the correct way to implement this. you should take a look at the MVVM Pattern.
Your UI is not the proper place to "store extra data". UI is UI and data is data. This is the worst mistake done by people coming from a winforms or otherwise non-WPF background, using a wrong approach and a wrong mindset in WPF.
This will either not work (because the ItemContainerGenerator of the TreeView knows nothing about your class, or require extra work in overriding the default behavior of such class.

Categories

Resources