Tedd Parsile Tedd Parsile - 9 days ago 5
C# Question

Wpf binding not working when notifying property change without assigning new instance to property

So, I have this very simle code, and when I click on a button while using the commented line of code, the value in GUI is through a binding updated and everything is ok. If I use the other two lines (uncomented), nothing happens even though I am notifing change.

public partial class MainWindow : Window, INotifyPropertyChanged
{
private Spot _position = new Spot(0, 0);
public Spot Position
{
get { return _position; }
set { _position = value; OnPropertyChanged("Position"); }
}

public MainWindow()
{
InitializeComponent();
win.DataContext = this;
}

public event PropertyChangedEventHandler PropertyChanged;

public void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

private void Button_Click(object sender, RoutedEventArgs e)
{
//Position = new Spot(5,5);
Position.UpdateX(5);
OnPropertyChanged("Position");
}
}

public class Spot
{
public Spot(int x, int y)
{
X = x;
Y = y;
}

public void UpdateX(int x)
{
X += x;
}
public void UpdateY(int y)
{
Y += y;
}

public override string ToString()
{
return string.Format("{0}; {1}", X, Y);
}

public int X { get; set; } = 0;
public int Y { get; set; } = 0;
}


XAML

<StackPanel x:Name="win" Orientation="Vertical" Margin="100">
<Label Content="{Binding Position, UpdateSourceTrigger=PropertyChanged}" Width="100" Height="25"></Label>
<Button Width="100" Height="25" Click="Button_Click">Click</Button>
</StackPanel>


Thanks in advance for any advice.

Answer

Here's the deal.

First, get rid of UpdateSourceTrigger=PropertyChanged: That flag governs when a Mode=TwoWay Binding updates its source property. Label.Content binds OneWay by default, because it's not an editable control. It can't update its source. So leave that out, it's a no-op.

Second:

<Label Content="{Binding Position}" ... />

What this will do is call ToString() on Position. But it won't call ToString() again if the value of Position hasn't changed. And in the relevant sense, it hasn't: It's still the same instance of Spot that it was the last time ToString() was called. This is probably an "optimization".

This will work:

<Label>
    <TextBlock>
        <TextBlock.Text>
            <MultiBinding StringFormat="{}{0}: {1}">
                <Binding Path="Position.X" />
                <Binding Path="Position.Y" />
            </MultiBinding>
        </TextBlock.Text>
    </TextBlock>
</Label>

This will work too:

<Label>
    <TextBlock>
        <TextBlock Text="{Binding Position.X, StringFormat='{}{0}: '}" />
        <TextBlock Text="{Binding Position.Y}" />
    </TextBlock>
</Label>

I put the TextBlocks in Labels because you used a Label, and Label often has default margin/padding/whatever styling that is relevant for the layout to look right.

This will not work, because Label.Content is Object, not String, so StringFormat will be ignored. Instead, you'll get an exception in the VS Output pane about MultiBinding needing a converter:

<Label
    >
    <Label.Content>
        <MultiBinding StringFormat="{}{0}: {1}">
            <Binding Path="Position.X" />
            <Binding Path="Position.Y" />
        </MultiBinding>
    </Label.Content>
</Label>

The ordinary way to do this is have Spot implement INotifyPropertyChanged. If the overall usage of Spot is such that it's not appropriate to do that, you're stuck doing something more or less ugly. Myself, I'd give Position's owner an UpdatePosition(int deltaX, int deltaY) method that creates a new instance of Spot and assigns it to Position. If a property of the viewmodel (or a window masquerading as a viewmodel) doesn't implement INotifyPropertyChanged, treat the property value as immutable.