Visible Binding Based On Bound Object and Model Properties - c#

I ran in to the unique situation today where I needed to bind the Visible property of a button in a DataGridRow to be based on both a property of the bound object and of the model backing it.
XAML:
<t:DataGrid ItemsSource="{Binding Items}">
<t:DataGrid.Columns>
<t:DataGridTemplateColumn>
<t:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Visibility="IsEditable OR IsAdmin"/>
</DataTemplate>
</t:DataGridTemplateColumn.CellTemplate>
</t:DataGridTemplateColumn>
</t:DataGrid.Columns>
</t:DataGrid>
Model:
class TheModel
{
public ObservableCollection<Whatever> Items { get; set; }
public bool IsAdmin { get; set; }
}
Class:
class Whatever
{
public bool IsEditable { get; set; }
}
This stumped me. The only concept that I could think might work would be somehow passing the bound object and either the entire model or just the IsAdmin property to a static method on a converter or something. Any ideas?

Firstly, you cannot directly use a Boolean for Visibility. You need to use the BooleanToVisibilityConverter.
Secondly, about the OR, you have different options:
Create a readonly property IsEditableOrAdmin in Whatever which returns the value you want. Drawback: Your Whatever will need a back-reference to TheModel.
Use a MultiBinding and write an IMultiValueConverter. Then, pass both values in the MultiBinding. Since TheModel is no longer in the DataContext scope at that point, you could use the ElementName property of the Binding to refer to a UI element where TheModel is still accessible.
Example (untested):
<SomeElementOutsideYourDataGrid Tag="{Binding TheModel}" />
...
<Button>
<Button.Visibility>
<MultiBinding Converter="{StaticResource yourMultiValueConverter}">
<Binding Path="IsEditable" />
<Binding ElementName="SomeElementOutsideYourDataGrid" Path="Tag.IsAdmin"/>
</MultiBinding>
</Button.Visibility>
</Button>
Use a more powerful binding framework such as PyBinding.

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}}"/>

DependencyProperty does not work if the value is from a binding

I created UserControl with viewmodel. It has DependencyProperty which only works if the value is passed directly. If the value is passed through the binding, it no longer works.
Here is the view code:
This is a closed element not associated with any other. All listed items belong to him. This is a code shortening, I am not going to present whole, immeasurable structures.
View
public partial class SomeView : UserControl
{
public SomeView()
{
InitializeComponent();
SetBinding(ActiveProperty, new Binding(nameof(SomeViewModel.Active)) { Mode = BindingMode.OneWayToSource });
}
#region ActiveProperty
public static readonly DependencyProperty ActiveProperty = DependencyProperty.Register(nameof(Active), typeof(bool), typeof(VNCBoxView));
public bool Active
{
get { return (bool)GetValue(ActiveProperty); }
set { SetValue(ActiveProperty, value); }
}
}
VievModel
public class SomeViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool active;
public bool Active
{
get { return active; }
set
{
active = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Active)));
}
}
}
UserControl
<UserControl ...>
<UserControl.DataContext>
<viewModels:SomeViewModel />
</UserControl.DataContext>
<Grid>
<TextBlock Text="{Binding Active}" />
</Grid>
</UserControl>
===================================================
When working with a ready component, which is an individual, separate entity, the problem occurs depending on how it is used.
I remind you that the elements used in the view in question are a closed whole that does not connect with the element in which it is used. It is the transfer of value that is the matter of the problem.
This is working usage:
<local:SomeView Active="True" />
In viewmodel, the setter is invoked twice, once with false and then with true.
If the value comes from binding, it doesn't work:
<local:SomeView Active="{Binding SomeParentProperty}" />
In viewmodel, setter is only called once with the value false.
Setters in a view are never called, in both cases.
Please help
There is no IsConnected property in the SomeViewModel instance in the current DataContext of the UserControl, hence the Binding
<local:SomeView Active="{Binding IsConnected}" />
won't work. It tries to resolve the PropertyPath against the current DataContext, unless you explicitly specify its Source, RelativeSource or ElementName.
This is the exact reason why UserControls should never explicitly set their own DataContext, and hence never have something like an own, private view model.
The elements in the UserControl's XAML would not bind to properties of such a private view model object, but directly to the properties of the UserControl, for example like
<TextBlock Text="{Binding Active,
RelativeSource={RelativeSource AncestorType=UserControl}}"/>
When you set the DataContext explicitly in the UserControl like this:
<UserControl.DataContext>
<viewModels:SomeViewModel />
</UserControl.DataContext>
...you can no longer bind to SomeView's DataContext in the consuming view like this:
<local:SomeView Active="{Binding IsConnected}" />
...because SomeViewModel doesn't have any IsConnected property.
You should avoid setting the DataContext explicitly and let the UserControl inherit its DataContext from its parent element. You can still bind to the dependency property of the UserControl itself using a RelativeSource or an ElementName:
<UserControl ...>
<Grid>
<TextBlock Text="{Binding Active, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</Grid>
</UserControl>
Besides, SomeViewModel seems superfluous in your example since the UserControl already has an Active property.

WPF Multibinding: OneWayToSource binding from TextBox updated via another binding doesnt work?

I have a DataGrid bound to the People collection. Also I have a TextBox that should accept the Name value from the selected row. User can then edit the value or can leave it as is. The key point is: the text shown in the TextBox no matter whether it originates from collection or user typing must be propagated to the property NewName.
I've set two bindings for the NewNameTextBox: OneWay'ed to the CollectionView behind the DataGrid, and OneWayToSource'ed to the property:
<Window.Resources>
<CollectionViewSource x:Key="PeopleCollection"
Source="{Binding Path=People, Mode=OneWay}" />
<local:ConverterNewNamePrefill x:Key="ConverterNewNamePrefill" />
</Window.Resources>
<Grid>
<StackPanel>
<DataGrid ItemsSource="{Binding Source={StaticResource PeopleCollection}}"
AutoGenerateColumns="True"
IsReadOnly="True"
Margin="10">
</DataGrid>
<StackPanel Orientation="Horizontal" Margin="10">
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource ConverterNewNamePrefill}" >
<Binding Source="{StaticResource PeopleCollection}" Path="Name" Mode="OneWay" />
<Binding Path="NewName" Mode="OneWayToSource" />
</MultiBinding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</Grid>
I suppose the property should be updated when user changes selection in the DataGrid, but this doesn't happen. The TextBox gets updated and shows the selected Name value, but the property bound via OneWayToSource remains unchanged.
If the user types into the TextBox, the property gets updated as expected.
So the question is how can I update a property from both the sources via multi-bound TextBox without code behind view?
Here is the code behind window:
public class Person
{
public string Name { get; set; }
public string Surname { get; set; }
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private ObservableCollection<Person> _people = new ObservableCollection<Person> {
new Person() {Name = "Mitchell", Surname = "Sofia" },
new Person() {Name="Bush", Surname="Ethan" },
new Person() {Name="Ferrero", Surname="Emma" },
new Person() {Name="Thompson", Surname="Aiden" }
};
public ObservableCollection<Person> People => _people;
public string NewName { get; set; } = "Jackson";
}
public class ConverterNewNamePrefill : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new[] { value, value };
}
}
The converter's method ConvertBack() is called only when user types, but not when the TextBox.Text updated from collection.
Thank you!
This is just how bindings work. The source or sources are not updated unless the target changes by means other than the binding itself. I.e. it's assumed that if the target was just updated from the source(s), then the source(s) is(are) already up-to-date and do not need updating.
Without more details it's difficult to know for sure what you want. But it seems like you might either want for NewName to actually be the target of a second binding, where the source is the same Name property being used as the source for the TextBox.Text property, or you want subscribe to the TextBox.TextChanged event and your handler explicitly write back the value to the NewName property when that event is raised.
In the former case, you'll have to make NewName a dependency property of MainWindow. That's a complication you may or may not want to deal with. If not, then I'd recommend the latter approach.

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}"/>

How to set a property of a property in XAML?

I have a user-control as my view (named MyView) and it has it's data context set to an instance of my view-model (of type MyViewModel).
I have in my view's code-behind a read-only property for it (which is the MVVM-Light snippet) that looks like so:
public MyViewModel Vm
{
get { return (MyViewModel) DataContext; }
}
MyViewModel has a property named Title of type string, and I want to change it through XAML because MyView is being used as an ItemTemplate for a ListBox.
<ListBox ItemsSource="{Binding MyViewModelCollection}">
<ListBox.ItemTemplate>
<DataTemplate>
<Controls:MyView /> <!-- How do I set Vm.Title property here? -->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
How can I do this?
or perhaps there is a better way?
Could you simply make a property within your MyView that reflects down to the view model?
public string Title
{
get { return ((MyViewModel) DataContext).Title; }
set { ((MyViewModel) DataContext).Title = value; }
}
Then write:
<Controls:MyView Title="MyTitle" />
If you want to bind the title, you'll have to make it a dependency property not a regular property.

Categories

Resources