πŸ“œ ⬆️ ⬇️

WPF Binding: When do you need to use ObjectDataProvider?

There are many ways to create an object that will be used as a data source for binding. Many people create an object in code and assign this object to the DataContext property of Window. In general, this is a good way. You may have noticed that I added the source object to the Window's Resource Dictionary in most of my posts, and it worked quite well. However, we have the ObjectDataProvider class in the data binding, which can also be used to create your source object in XAML. In this post I will try to explain the differences between adding a source object directly to resources and using an ObjectDataProvider. I hope I will give you a guide on how to evaluate your task and choose the best solution.

As I describe these possibilities, we will create a small program that will allow people to indicate their weight on Earth and find out how much they will weigh on Jupiter.

When we add a source object directly to resources, the data binding engine calls the default constructor for the type of this object. The object added to the resource dictionary uses the key defined through x: Key. Here is an example code with this approach:
< Window.Resources > < local:MySource x:Key =” source ” /> (…) </ Window.Resources > * This source code was highlighted with Source Code Highlighter .
  1. < Window.Resources > < local:MySource x:Key =” source ” /> (…) </ Window.Resources > * This source code was highlighted with Source Code Highlighter .
  2. < Window.Resources > < local:MySource x:Key =” source ” /> (…) </ Window.Resources > * This source code was highlighted with Source Code Highlighter .
  3. < Window.Resources > < local:MySource x:Key =” source ” /> (…) </ Window.Resources > * This source code was highlighted with Source Code Highlighter .
  4. < Window.Resources > < local:MySource x:Key =” source ” /> (…) </ Window.Resources > * This source code was highlighted with Source Code Highlighter .
< Window.Resources > < local:MySource x:Key =” source ” /> (…) </ Window.Resources > * This source code was highlighted with Source Code Highlighter .

Alternatively, you can add an ObjectDataProvider class object to resources and use it as a source for binding. The ObjectDataProvider class is a wrapper for your source object, which provides some additional functionality. Next, I will talk about the following remarkable features of the ObjectDataProvider:

Passing parameters to the constructor


When we add a source object directly to resources, WPF always calls the default constructor for this type. It may happen that you do not have control over the source object, and the class that you add does not have a default constructor. For example, this is a class from a third-party library and it is declared as sealed. In this situation, you can create an instance of a class in XAML by using an ObjectDataProvider in the following way:
  1. < ObjectDataProvider ObjectType = ”{ x: Type local: MySource }” x: Key = ” odp1 β€³ >
  2. < ObjectDataProvider.ConstructorParameters >
  3. < system: String > Jupiter </ system: String >
  4. </ ObjectDataProvider.ConstructorParameters >
  5. </ ObjectDataProvider >
* This source code was highlighted with Source Code Highlighter .

This markup creates an instance of the class MySource by invoking its constructor and passing the string β€œJupiter” as its parameter. This also creates an object of class ObjectDataProvider, which will wrap an object of class MySource.

MySource has a public property called β€œPlanet” that provides an object of the Planet class, whose name corresponds to the string passed to the constructor. In our case, this is β€œJupiter”. I want the program to have a label that associates with the Name property of the planet. Binding to subproperties can be implemented in WPF using β€œdot notation”. In general, it looks like this: Path = Property.SubProperty. You can see this in the following code:
  1. < Label Content = ”{ Binding Source = { StaticResource odp1 }, Path = Planet . Name } ” Grid . ColumnSpan = ” 2 β€³ HorizontalAlignment =” Center ” FontWeight =” Bold ” Foreground =” IndianRed ” FontSize =” 13 β€³ Margin = ” 5 , 5 , 5 β€³ 15 β€³ />
* This source code was highlighted with Source Code Highlighter .

You might look at this binding definition and think that it does not make sense. It looks as if we are associating with the Name sub-property of the Planet property of the ObjectDataProvider class. But I mentioned above that MySource has a Planet property, but ObjectDataProvider does not have it. The reason why this code works is that the binding engine accesses the ObjectDataProvider in a special way when the Path property of the source object is wrapped (that is, wrapped). Note that this special access method can also be applied when binding to an XmlDataProvider or CollectionViewSource.
')

Method binding


MySource has a method that takes as an argument the weight of a person on Earth and calculates the weight of this person on the planet, which was transferred to the constructor. I want to pass some weight value to a method in XAML and communicate with its result. This can be done using ObjectDataProvider:
  1. < ObjectDataProvider ObjectInstance = ”{ StaticResource odp1 }” MethodName = ” WeightOnPlanet ” x: Key = ” odp2 β€³ >
  2. < ObjectDataProvider.MethodParameters >
  3. < system: Double > 95 </ system: Double >
  4. </ ObjectDataProvider.MethodParameters >
  5. </ ObjectDataProvider >
* This source code was highlighted with Source Code Highlighter .

Notice that instead of setting the ObjectType property, this time I set the ObjectInstance property. This allows us to reuse an instance of the class MySource, which we created in the previous ObjectDataProvider. I also set the MethodName property and passed the parameter to this method using the MethodParameters property. Displaying the result returned from a method is as simple as creating a binding with this second ObjectDataProvider:
  1. < Label Content = ”{ Binding Source = { StaticResource odp2 }}” Grid . Row = ” 2 β€³ Grid . Column = ” 1 β€³ Grid . ColumnSpan = ” 2 β€³ />
* This source code was highlighted with Source Code Highlighter .

This is a good start, but I would like to allow users to enter their own weight in TextBox, and so that the Label will show the new value. However, the problem is that I cannot put the Binding description in the MethodParameters property because this property is not a DependencyProperty. In fact, ObjectDataProvider is also not a DependencyObject. As you remember, the target of the Binding should be a Dependency property, although there can be any source. Fortunately, there is a way out when you want to bind the CLR property to the Dependency property: you can swap the source and target by placing your binding in the Dependency property and setting the Mode Binding property to TwoWay or OneWayToSource. This code shows how to create a TextBox that modifies the argument that was passed to the WeightOnPlanet method:
  1. < Window.Resources >
  2. (...)
  3. < local: DoubleToString x: Key = ” doubleToString ” />
  4. (...)
  5. </ Window.Resources >
  6. (...)
  7. < TextBox Grid . Row = ” 1 β€³ Grid . Column = ” 1 β€³ Name =” tb ” Style =” { StaticResource tbStyle } ” >
  8. < TextBox.Text >
  9. < Binding Source = ”{ StaticResource odp2 }” Path = ” MethodParameters [ 0 ]” BindsDirectlyToSource = ” true ” UpdateSourceTrigger = ” PropertyChanged ” Converter = ”{ StaticResource doubleToString }” >
  10. (...)
  11. </ Binding >
  12. </ TextBox.Text >
  13. </ Textbox >
* This source code was highlighted with Source Code Highlighter .

In this situation, I do not need to do anything for the binding to be Two-Way, because I create a binding with the Text property of an object of the TextBox class; it [binding] is already Two-Way by default. By default, the bindings will be One-Way in most Dependency properties and Two-Way in those properties where we expect the data to be changed by the user. This default behavior can be overridden by changing the Mode property of the Binding.

As I said above, when we create a binding on an ObjectDataProvider, the binding engine automatically looks at the source that is wrapped, and not at the ObjectDataProvider itself. In this situation, this presents a problem for us, because we want to connect with the ObjectDataProvider's MethodParameters property. To change the default behavior, we must set the BindsDirectlyToSource property to true.

The MethodParameters property is an IList, and in our situation we want to bind to the first item in the list, since the WeightOnPlanet method takes only one parameter. We can do this using an indexer, just as we would do in a C # code.

I set the property UpdateSourceTrigger to PropertyChanged, so when the method is invoked, we get a new value whenever the user types something in the TextBox. Other possible values ​​for the UpdateSourceTrigger property are β€œExplicit” (we must explicitly call UpdateSource () on the binding) and β€œLostFocus” (the source will be updated when the control loses focus), which is the default.

If we were binding to a property with a double type, the binding engine would try to automatically convert the Text Text Box's property to a double type. However, due to the fact that we are attached to the method, we need to write our own converter. Without it, the binding will try to call the WeightOnPlanet method, which accepts a string as a parameter, which will lead to an error, because there is no such method. If we look at the Output window in Visual Studio, we will see a debugging message there that says that we could not find a method that accepts the parameters that we pass. Here is the code for this converter:
  1. public class DoubleToString: IValueConverter
  2. {
  3. public object Convert ( object value , Type targetType, object parameter, System.Globalization.CultureInfo culture)
  4. {
  5. if ( value ! = null )
  6. {
  7. return value .ToString ();
  8. }
  9. return null ;
  10. }
  11. public object ConvertBack ( object value , Type targetType, object parameter, System.Globalization.CultureInfo culture)
  12. {
  13. string strValue = value as string ;
  14. if (strValue! = null )
  15. {
  16. double result;
  17. bool converted = Double.TryParse (strValue, out result);
  18. if (converted)
  19. {
  20. return result;
  21. }
  22. }
  23. return null ;
  24. }
  25. }
* This source code was highlighted with Source Code Highlighter .

Some of you may be a little puzzled by this converter: should it be StringToDouble or DoubleToString? The Convert method is called when data is transferred from a source (double) to a target (string), and the ConvertBack method is called when a transfer occurs in the opposite direction. And so, we need a DoubleToString converter, and not any other.

And what will happen if the user enters the wrong weight value? He may enter a negative number, or a string containing more than just numbers, or he may not enter anything at all. And, if the situation is this, I do not want to allow the binding to begin transmitting data to the source. I want to create my own logic, which not only prevents the binding from transmitting data, but also warns the user that the value entered by him is incorrect. This can be done using the Validation function in data binding. I wrote a ValidationRule that validates the values ​​and added it to the property in the following way:
  1. < Binding Source = ”{ StaticResource odp2 }” Path = ” MethodParameters [ 0 ]” BindsDirectlyToSource = ” true ” UpdateSourceTrigger = ” PropertyChanged ” Converter = ”{ StaticResource doubleToString }” >
  2. < Binding.ValidationRules >
  3. < local: WeightValidationRule />
  4. </ Binding.ValidationRules >
  5. </ Binding >
* This source code was highlighted with Source Code Highlighter .

WeightValidationRule is a heir from ValidationRule and overrides the Validate method, in which I described the logic I needed:
  1. public class WeightValidationRule: ValidationRule
  2. {
  3. public override ValidationResult Validate ( object value , System.Globalization.CultureInfo cultureInfo)
  4. {
  5. // Value is not a string
  6. string strValue = value as string ;
  7. if (strValue == null )
  8. {
  9. // not going to happen
  10. return new ValidationResult ( false , β€œInvalid Weight - Value is not a string ”);
  11. }
  12. // Value can not be converted to double
  13. double result;
  14. bool converted = Double.TryParse (strValue, out result);
  15. if (! converted)
  16. {
  17. return new ValidationResult ( false , β€œInvalid Weight - Please type a valid number”);
  18. }
  19. // Value is not between 0 and 1000
  20. if ((result <0) || (result> 1000))
  21. {
  22. return new ValidationResult ( false , β€œInvalid Weight - You're either too light or too heavy”);
  23. }
  24. return ValidationResult.ValidResult;
  25. }
  26. }
* This source code was highlighted with Source Code Highlighter .

Now, entering the wrong value causes a red border around the TextBox. However, I want to notify the user about the message that is contained in the ValidationResult. Moreover, I would like a tooltip to appear with an error message when the user does something wrong. This can all be done in XAML, with styles and triggers:
  1. < Style x: Key = ” tbStyle ” TargetType = ”{ x: Type TextBox }” >
  2. < Style.Triggers >
  3. < Trigger Property = ” Validation . HasError ” Value =” true ” >
  4. < Setter Property = ” ToolTip ”
  5. Value = ”{ Binding RelativeSource = { RelativeSource Self }, Path = ( Validation . Errors ) [ 0 ]. ErrorContent } ” />
  6. </ Trigger >
  7. </ Style.Triggers >
  8. </ Style >
* This source code was highlighted with Source Code Highlighter .

Validation.HasError is an attached Dependency property that becomes true whenever at least one ValidationRule returns an error. Validation.Errors is also a nested Dependency property that contains a list of errors for a particular element. In this case, we know that a TextBox can have only one error (since it has only one rule), which means that we can safely associate ToolTip with the first error in this list. β€œ{RelativeSource Self}” simply means that the source of our binding is TextBox itself. Notice that the description of the Path property uses parentheses β€” they should be used whenever we bind to the nested Dependency property. In Russian, β€œPath = (Validation.Errors) [0] .ErrorContent” means that we are looking for the source property Validation.Errors (i.e., TextBox), we get the first item in the list and refer to ValidationError's apt property ErrorContent.

If you try to enter something in TextBox that is different from a number in the range from 0 to 1000, then you should see a pop-up hint with an error message.

I wrote an example (which is included in the SDK) that shows some other aspects of validation. There is a lot more to say about this possibility, but I will leave a more detailed explanation for the subsequent posts.

Replacing the source object


You may need to, when you need to replace the current source of all your bindings with some other object. If you have an object in the resource dictionary that is used as a source for linking, there is no way to replace this object with any other object and cause an update of all bindings. Deleting this object from the resource dictionary and adding a new one with the same x: Key will not cause the bindings to be notified.

If this is your case, you can decide to use ObjectDataProvider. If you change the ObjectType property, all bindings to this ObjectDataProvider will be notified that the source object has been updated and the data must be updated.

Note that if you assign the DataContext property of an element that is higher in the element tree, your source object is programmatically, assigning another object will also cause all bindings to be updated.

Asynchronous source object creation


ObjectDataProvider has a property called IsAsynchronous that allows you to control whether data will be loaded in the same stream as your program, or in another. By default, ObjectDataProvider has this property set to false, and XmlDataProvider set to true.

I plan to discuss this in more detail in one of my future posts, so be prepared.

You can use the source code of this example in order to write a WPF program that will allow users to select a planet, enter their weight and find out their weight on the selected planet. In fact, it is very simple.


Here you can find a project for Visual Studio with the code that was used in the article.

Source: https://habr.com/ru/post/46998/


All Articles