Simple TextBox Text Binding Fails WPF - c#

We are converting an app from Silverlight to WPF. It's a fairly complex app, but the code sharing is about 95% +. The XAML is pretty much all the same except for XML namespace definitions etc. About 90% of the app now works but there are a few glaring issues that are puzzling me. One is this binding issue.
We have a model object called TaskInfo. It has a property called TaskNo. in Silverlight and WPF we bind to this property like this
<TextBox IsReadOnly="True" Grid.Column="0" Grid.Row="0" Margin="1" Text="{Binding Path=TaskNo}" Height="28" Background="#CAECF4" VerticalAlignment="Center" VerticalContentAlignment="Center" />
In both WPF and Silverlight the TaskNo is correctly displayed when the TaskInfo model is first set as the DataContext. In Silverlight, if we create a new TaskInfo, send it to the server for saving, and return the model with a new TaskNo, the TaskNo is successfully displayed. But, in WPF, it just displays 0 when the saved TaskInfo is returned from the server. There is some issue with binding. This is the binding error I see in the output window:
System.Windows.Data Information: 10 : Cannot retrieve value using the
binding and no valid fallback value exists; using default instead.
BindingExpression:Path=TaskNo; DataItem=null; target element is
'TextBlock' (Name=''); target property is 'Text' (type 'String')
I inspected the visual tree and the TextBox's DataContext is set to the TaskInfo as expected.
So, I turned off binding and tried this code. It's the event handler for the DataContextChanging on the TextBox. This code works fine. When a new task is saved and returned, the TaskNo successfully displays here:
private void TaskNoBox_DataContextChanging(object sender, DependencyPropertyChangedEventArgs e)
{
var task = TaskNoBox.DataContext as TaskInfo;
if (task == null)
{
throw new Exception("Ouch!");
}
TaskNoBox.Text = task.TaskNo.ToString();
}
To further debug this problem, I added this event handler for the GotFocus event on the text box. So, after the task has been saved on the server side and has been returned and set as the DataContext, I click inside the control to fire this event handler. When I step through this code, I can see that the DataContext is correct, and has the correct TaskNo. Calling this code still doesn't cause the binding to occur.
private void TextBox_GotFocus(object sender, RoutedEventArgs e)
{
var textBox = (TextBox)sender;
var be = textBox.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
be.UpdateTarget();
}
TextBox Text binding property:
DataContext of TextBox's properties:
How do I make sense of this binding error? What are the binding gotchas between Silverlight and WPF? Do I need some kind of workaround? Why is binding not working?

Binding in WPF never updates if the previous DataContext is equivalent to the new DataContext according to the Equals method.
The difference between Silverlight and WPF seems to be that when the DataContext changes, WPF seems to use the Equals method to evaluate difference between objects while Silverlight uses the reference. That means that WPF is the same as Xamarin.Forms.
I tried this code, and it causes the TaskNo to display correctly. I think what is happening is that because the previous DataContext was equivalent to the new DataContext when Equals is called. So, this works around the problem.
private async void TaskPageHeader_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
TaskNoBox.DataContext = new object();
TaskNoBox.DataContext = CurrentTask;
}

Related

Excel/VSTO: error 0x80028018 only when binding directly to interop.-provided property

When I try to set Worksheet's Name property using this test application, it doesn't work due to an 0x80028018 error.
When I change the value in the first text box, and I click the tab button, nothing happens, while I expect the Sheet1 to be renamed. In the Visual Studio Output Window I can see the following error:
System.Windows.Data Error: 8 : Cannot save value from target back to source.
BindingExpression:Path=Worksheet.Name; DataItem='ViewModel' (HashCode=35209123); target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
ExternalException:'System.Runtime.InteropServices.ExternalException (0x80028018): Exception of type 'System.Runtime.InteropServices.ExternalException' was thrown.
at System.Windows.Forms.ComponentModel.Com2Interop.Com2PropertyDescriptor.SetValue(Object component, Object value)
at MS.Internal.Data.PropertyPathWorker.SetValue(Object item, Object value)
at MS.Internal.Data.ClrBindingWorker.UpdateValue(Object value)
at System.Windows.Data.BindingExpression.UpdateSource(Object value)'
The application is a very basic WPF application with a ViewModel class that contains the reference to the Workbook. Then we have a view (xaml) class that binds to the ViewModel.
Viewmodel class:
public class ViewModel
{
public Worksheet Worksheet { get; set; }
public ViewModel(Worksheet worksheet)
{
Worksheet = worksheet;
}
public string Name
{
get => Worksheet.Name;
set => Worksheet.Name = value;
}
}
Then, in the view, I have a TextBox control that is bound to the Name property of the Worksheet.
Basically there are two ways of doing this:
<TextBox Text="{Binding Worksheet.Name}"/>
or:
<TextBox Text="{Binding Name}"/>
that's because my ViewModel exposes both Worksheet and Name.
And this is where things get interesting.
If I use the first way, it doesn't work and in the debug output of visual studio I find the error shown above.
If I use the second way it works just fine. This way is through Name property, that in turn means that I have an explicit
Worksheet.Name = ... in my code (in the Name's setter).
This is the only difference that I see between the two solutions.
Further analysis & questions
The 0x80028018 seems to be a well-known one; there are several articles talking about it.
I read this one:
HowTo: Fix “Old format or invalid type library” error (0x80028018)
but:
I can't explain why in my application I see two different behaviors
I can't figure out how to fix the problem at application level so that the binding works as expected
Even if the problem is not so critical and there is an easy workaround for it, there is the risk that it could indicate greater issues. That's why I'm looking for a robust solution.
Left over code, if you want to reproduce it
Beyond the ViewModel class there is the very easy xaml view, (only note: implemented as a UserControl because it has to be loaded into an ElementHost).
There is an exceeding TextBox just for allowing the first to lose focus and trigger the update.
<UserControl x:Class="ExcelAddIn12.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ExcelAddIn12"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel>
<!--the non-working solution: -->
<TextBox Text="{Binding Worksheet.Name}"/>
<TextBox/>
</StackPanel>
</UserControl>
It remains only the launch & wiring code. As a launcher i used a Ribbon (designed, not xaml) with a button.
public partial class Ribbon1
{
private void Ribbon1_Load(object sender, RibbonUIEventArgs e) { }
private void button1_Click(object sender, RibbonControlEventArgs e)
{
Worksheet myWorksheet = (Worksheet)Globals.ThisAddIn.Application.ActiveSheet;
ViewModel vm = new ViewModel(myWorksheet);
UserControl1 view = new UserControl1() { DataContext = vm };
Form form = new Form();
ElementHost wpfHost = new ElementHost() { Dock = DockStyle.Fill, Child = view };
form.Controls.Add(wpfHost);
form.Show();
}
}
Versions:
Visual Studio 2017 (EN)
Excel 2010 (EN)
The article you linked states the following:
Most of the Excel Object Model methods and properties require specifying an LCID (locale identifier). If a client computer has the English version of Excel, and the locale for the current user is configured for another language (e.g. German or French), Excel may fire the “Old format or invalid type library” exception with error code 0x80028018 (-2147647512).
It also tells you to set the System.Threading.Thread.CurrentThread.CultureInfo to the one Excel uses. The simplest way to do this is to do it once globally for the current (UI) thread:
Thread.CurrentThread.CurrentCulture = new CultureInfo(Application.LanguageSettings.LanguageID[MsoAppLanguageID.msoLanguageIDUI])
As for the reason this is happening, it looks like a different LCID is used when calling directly through binding and when calling from your wrapper property. You might want to check your Thread.CurrentThread.CurrentCulture and Thread.CurrentThread.CurrentUICulture properties when those calls are made.
Here are a couple of links for a bit more background information about the different culture settings:
Setting Culture (en-IN) globally in WPF application
#763 – The Difference Between CurrentCulture and CurrentUICulture
Application Culture / UICulture
WPF Bindings and CurrentCulture Formatting
In short, CurrentThread.CurrentCulture is your current region setting (that you can change via control panel), CurrentThread.CurrentUICulture corresponds to the language of Windows that you installed (that you usually cannot change easily), CultureInfo.DefaultThreadCurrentCulture will change the current CurrentThread.CurrentCulture property and setup the default value for future threads and finally there is something like the following line, that applies to WPF bindings:
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(
XmlLanguage.GetLanguage(
CultureInfo.CurrentCulture.IetfLanguageTag)));
Of course you could also just change your local users culture to the one Excel is using (Englisch).

WPF PropertyChanged in UserControl DependencyProperty not fired every time

I've been researching for many hours the following situation: I have a xaml defined window that makes use of a usercontrol (ToggleButton) with some dependency properties.
The underlying viewmodel of the window contains some boolean objects that represent the state of devices (on/off) and others represent a request to toggle a device with a true/false flank (a PLC is connected to these and communication works fine).
Hence there are 2 DP's on the usercontrol:
The one to toggle the devices (binding mode OneWayToSource with an UpdateSourceTrigger.Explicit work fine (indicating to me that basics like shared DataContext is fine and not "disrupted" anywhere).
However the binding indicating the other DP (device state with binding mode OneWay) shows the following symptoms:
The (PLC-)device is off (false) before starting the program
Result: The DeviceState property is at the default value of false.
Set is called the first time when the device is switched on
(underlying viewmodel object changes to true, reports this via
PropertyChanged notification) and the DependencyPropertyChanged is
being called correctly. Further switches to off/on (false/true)
again don't result in "set" being called again (although
PropertyChanged on the underlying object is again called).
The device is on (true) before starting the program
Result: The DP Handler is triggered at
the start of the program and no change to
false or true lets it be called again.
What I've tried already for tracking this down is:
Implemented a DummyDebugConverter.
Result: I see that it's fired also only once. So giving me no further clue
Analyzed the Output Window and found the following message:
System.Windows.Data Information: 21 : BindingExpression cannot retrieve value from null data item. This could happen when binding is detached or when binding to a Nullable type that has no value. BindingExpression:Path=bLightState.Value; DataItem='ControlPanelModel' (HashCode=45596481); target element is 'AdsButton' (Name='btnLight'); target property is 'DeviceState' (type 'Boolean')
Debugging this didn't give me a clue. My breakpoints e.g. in the debugging converter or the set method never showed me a null-value anywhere. All values in the viewmodel constructor are initialized with default values. But I see the message always just one single time and I assume it relates to the problem somehow.
Used the same binding expression for testing purposes on some other elements (a label and a toggle button) besides my usercontrol. They work nicely and are updating their values as expected as soon as the object in the viewmodel changes (desired behaviour). The message in 2 disappers if I remove my usercontrol.
So I come to the conclusion that the error is in my definition of the DP's.
Here are the relevant code snippets:
AdsButton.xaml.cs
[Description("When set to true the device is shown as on"), Category("Default")]
public bool DeviceState
{
get { return (bool)GetValue(DeviceStateProperty); }
set { SetValue(DeviceStateProperty, value); }
}
public static readonly DependencyProperty DeviceStateProperty =
DependencyProperty.Register(
"DeviceState", typeof(bool),
typeof(AdsButton),
new FrameworkPropertyMetadata(
false,
FrameworkPropertyMetadataOptions.None,
DeviceStateChanged,
CoerceDeviceStateProperty,
true,
UpdateSourceTrigger.Explicit));
private static void DeviceStateChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
(d as AdsButton).DeviceState = (bool) e.NewValue;
}
private static object CoerceDeviceStateProperty(DependencyObject d, object value)
{
return value ?? false;
}
ControlPanel.xaml
<src:AdsButton x:Name="btnLight"
Value="{Binding Path=bLight.Value, Mode=OneWayToSource}"
DeviceState="{Binding Path=bLightState.Value, Mode=OneWay}" />
<Label Content="{Binding bLightState.Value, Mode=OneWay}" />
<ToggleButton Content="Button" IsChecked="{Binding bLightState.Value, Mode=OneWay}" />
So does anybody know: Why is my own DP reacting differently from the ones in standard controls?
Thanks to the initial comment by Roger... the answer is obvious:
Setting the DP itself in the setter method overwrites the binding with a fixed value (which effectively removes it), but only if the new value is different from the old one.

WPF binding in ItemTemplate works but produces error messages

I am using Mahapps.Metro.Controls.DropDownButton in a UserControl in my project, which I populate using data binding. In order to know which item gets selected, I apply an item template in which I specify the item click handler. The relevant XAML is
<Controls:DropDownButton
x:Name="selector"
VerticalContentAlignment="Center"
Content=" "
Background="Transparent"
BorderThickness="0"
ItemsSource="{Binding Catalogues}"
>
<Controls:DropDownButton.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Id}" MouseDown="HandleDropDownItemMouseDown" />
</DataTemplate>
</Controls:DropDownButton.ItemTemplate>
</Controls:DropDownButton>
The DataContext is a custom view model set in the constructor in the code-behind for the user control:
public CatalogueEditor()
{
InitializeComponent();
this.viewModel = new CatalogueEditorViewModel();
this.DataContext = this.viewModel;
}
The Catalogues property in the view model is a custom KeyedCollection<string, Catalogue> that implements INotifyCollectionChanged. This contains elements that are similar custom KeyedCollection objects implementing INotifyCollectionChanged, but with item type Question, which is no longer a collection. Catalogue objects have a read-only property Id, to which I bind the TextBlock in the item template.
The binding seems to work all right and the DropDownButton gets populated with the Id labels of the Catalogue objects in the Catalogues collection, yet I get an output informing me of a binding error:
System.Windows.Data Error: 40 : BindingExpression path error: 'Id' property not found on 'object' ''String' (HashCode=-842352768)'. BindingExpression:Path=Id; DataItem='String' (HashCode=-842352768); target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')
This tells me that at some point the DataContext of the TextBlock in the item template is perceived as a String, though I intend it to be a Catalogue as an item in the collection bound to the ItemsSource. It is not only the correct operation that corroborates this notion but also my HandleDropDownItemMouseDown event handler:
void HandleDropDownItemMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left && selector.IsExpanded) {
Catalogue catalogue = ((TextBlock)e.Source).DataContext as Catalogue;
if (catalogue != null) {
viewModel.Select(catalogue);
}
}
}
Placing a breakpoint here I can see that the DataContext of the TextBlock is indeed a Catalogue and the code works as intended.
Why does this apparent error message occur? Shall I worry about it, does it signify that I made some insidious mistake in my code, or shall I be content that the code works? My apologies if this is an irrelevant or stupid question, but I am just learning WPF and I find it quite challenging, so I try to understand what happens around me even if my code happens to work. Your insights are much appreciated.
The offending line is Content=" ". You are setting content of a control to " " string, to which control tries to apply your template. As string class has no Id property, it results in a binding error.

C# referencing a Grid in WPF to change properties

Hello im new to making apps with WPF and XAML in Visual Studio. So I have a grid I want to change its properties in the code.
My Grid's properties:
<Grid HorizontalAlignment="Left"
Height="603"
Margin="0,51,0,0"
x:Name="usersPan"
VerticalAlignment="Top"
Width="1286">
How I have been trying to change its properties
this.usersPan.SetValue(Grid.WidthProperty, PAN_SIZE);
usersPan.SetValue(Grid.WidthProperty, PAN_SIZE);
usersPan.Width = 0;
usersPan.Visibility = Visibility.Collapsed;
When I try to do that^ it says null reference for userPan
Thanks
Noooooooo, Don't ever do that. Make a ViewModel that is bound to the Grid's Width property, and then just change the value.
My suspicion is that you do not need this at all. Have a look into containers, and how to position them.
In all of this years, there have been rare occasions I needed to do that and I suspect you do not need to. Tell me what you are doing.
EDIT:
You have a VM which needs to implement the NotifyPropertyChanged interface (I won't do that here, there are plenty of examples on hoew to do that)
public class MainVM
{
public ObservableCollection<TabVM> TabsVms {get;set;}
public int SelectedIndex {get;set}
}
bound to the control
<TabControl DataContext={TabsVMs} SelectedIndex="{Binding SelectedIndex}">
...
</TabControl>
And in runtime you create a couple of Tabs
var TabsVMs = new ObservableCollection<TabVM>();
TabsVMs.add(new TabVM());
TabsVMs.add(new TabVM());
TabsVMs.add(new TabVM());
Then in runtime you change the value of the index.
MainVm.SelectedIndex = 1
and the the coresponding tab will become selected.
EDIT:
I can also recommend you to use Fody for the MVVM notification.
Also, when it comes to bindings, I can recommend you to use WPF inspector. a handy little tool
The best way to write WPF programs is to use the MVVM (Model-View-View Model) design pattern. There are two (2) ideas behind MVVM:
Write as little code as possible in the view's code-behind and put all of the logic in the View Model object, using WPF's data binding feature to connect the properties of the View Model object to the view's controls.
Separate the logic from the display so you can replace the view with some other construct without having to change the logic.
MVVM is a huge topic on its own. There are lots of articles about it, and frameworks that you can use to build your program. Check out MVVM Light, for example.
Don't know exactly why Grid is invisible in code-behind, but You can access it's properties using events (but don't think it is perfect solution).
For example add to your grid event Loaded
<Grid HorizontalAlignment="Left"
Height="603"
Margin="0,51,0,0"
x:Name="usersPan"
VerticalAlignment="Top"
Width="1286"
Loaded="FrameworkElement_OnLoaded">
and then from code-behind you can access grid in next way:
private void FrameworkElement_OnLoaded(object sender, RoutedEventArgs e)
{
var grid = sender as Grid;
if (grid != null)
{
grid.Width = 0;
}
}
Better solution :
Add some boolean property to your ViewModel like public bool IsGridVisible{get;set;}
And bind it to your Grid
<Grid HorizontalAlignment="Left"
Height="603"
Margin="0,51,0,0"
x:Name="usersPan"
VerticalAlignment="Top"
Width="1286"
Visibility="{Binding Path=IsGridVisible, Converter={StaticResource BoolToVis}">
where BoolToVis is converter which converts true to Visible and false to Hidden. You can define it in App.xaml like :
<BooleanToVisibilityConverter x:Key="BoolToVis" />
I was able to do something like this so I can change properties outside of an event.
private Grid userGrid;
private void onUserGridLoaded(object sender, RoutedEventArgs e)
{
userGrid = sender as Grid;
}

WPF initialization issue

I have a class MyWindow which inherits from Window. Within MyWindow, I have the following method to execute once my OK button is clicked:
private void OKButton_Click(object sender, RoutedEventArgs e)
{
var be = NameBox.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
this.Close();
}
XAML:
<Button Content="OK"
Click="OKButton_Click"
HorizontalAlignment="Left"
Margin="175,473,0,0"
VerticalAlignment="Top"
Width="75"
RenderTransformOrigin="-0.04,0.5"/>
In a separate class where I initialize my UI window, I say
MainWindow window = new MainWindow(ViewModel);
window.Show();
However, as soon as window.Show() is executed, the subsequent code is executed and I cannot actually interact with my window to do what I need to do. I feel like this is just a misunderstanding in how to actually use WPF in a larger context...any help?
Window.ShowDialog is what is needed to view the page. But one doesn't get the binding information as you did; which should be changed as well.
When the textbox loses focus it will update the binding so the code
var be = NameBox.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
is not needed. (Is this a leftover form winform programming?) So I suggestion one not update a binding as such.
The only possible thing to do if the binding is not updated is to change the binding to use the mode of TwoWay which ensures a back and forth data transfer between the variable bound to and the textbox on the screen.

Categories

Resources