0
votes

I'm very new to WPF, (I started yesterday) and I'm very confused about data binding. I have a View Model for a Window, which contains a widget called Foo which has its own View Model.

The widget Foo binds its Visibility TwoWay (via a BooleanToVisibilityConverter) to a bool field Visible on its FooViewModel. FooViewModel implements INotifyPropertyChanged and fires a PropertyChanged event whenever Visible is set.

In the Xaml for the Window, it creates a Foo whenever a button is clicked. The Window's view model has another boolean field which is bound TwoWay to the Visibility of its instance of a Foo View. The view model of the WIndow implements INotifyPropertyChanged and fires PropertyChanged events whenever the boolean field is modified.

What I expect this to do is whenever the window's boolean property changes, the visibility of the Foo instance will be set. When this happens I expect the View Model of Foo to be updated, since Foo's visibility binding is two way. When the Foo View Model changes its boolean field I expect the View to change its visibility. Further, I expect the Window view model to be notified that its instance of Foo is no longer visible, and hence the View model of the Window will update its own boolean field. Is this a fundamental misunderstanding?

I post the (obfuscated) code below if it helps shed light on this misunderstanding. Thanks.

Window Xaml

<Window x:Class="XXX.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:XXX.ViewModel"
        xmlns:v="clr-namespace:XXX"
        Title="MainWindow" Height="350" Width="525" WindowStartupLocation="CenterOwner" WindowState="Maximized">
    <Window.Resources>
        <vm:AppViewModel x:Key="AppViewModel"/>
        <vm:TwoWayVisibilityConverter x:Key="BoolToVisibility" />
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource AppViewModel}}">
        <DockPanel>
            <Menu DockPanel.Dock="Top">
                <MenuItem Header="_Connection" Command="{Binding Authenticate}"/>
                <MenuItem Header="_About" Command="{Binding ShowAbout}"/>
                <MenuItem Header="_Logout" Command="{Binding Logout}"/>
                <MenuItem Header="_Configuration" Command="{Binding Configuration}"/>
                <MenuItem Header="_Info" Command="{Binding ShowInfo}"/>
            </Menu>
            <StackPanel>
            </StackPanel>
        </DockPanel>
        <Border HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Background="White"
                Padding="10"
                BorderThickness="0">
            <TextBlock Text="XXX"/>
        </Border>
        <Grid x:Name="Overlay" Panel.ZIndex="1000" DataContext="{Binding Source={StaticResource AppViewModel}}">
            <Border HorizontalAlignment="Stretch"
                VerticalAlignment="Stretch"
                Visibility="{Binding Path=Modal, Converter={StaticResource BoolToVisibility}, Mode=OneWay}"
                Background="DarkGray"
                Opacity=".7" />
                <v:Configuration HorizontalAlignment="Center"
                VerticalAlignment="Center" 
                Visibility="{Binding Path=ConfigurationVisible, Converter={StaticResource BoolToVisibility}, Mode=TwoWay}"/>
                <v:Connect HorizontalAlignment="Center"
                VerticalAlignment="Center" 
                Visibility="{Binding Path=AuthenticateVisible, Converter={StaticResource BoolToVisibility}, Mode=TwoWay}"/>
        </Grid>
    </Grid>

Window View Model

class AppViewModel : INotifyPropertyChanged
{
    [Import(typeof (IEventBus))] private IEventBus _bus;

    public AppViewModel()
    {
        Authenticate = new ForwardCommand(obj => ShowAuthenticationView(), obj => !AuthenticateVisible);
        Configuration = new ForwardCommand(obj => ShowConfigurationView(), obj => !ConfigurationVisible);
    }

    public bool Modal
    {
        get
        {
            return AuthenticateVisible || ConfigurationVisible;
        }
    }
    public ICommand Authenticate { get; set; }
    public bool AuthenticateVisible { get; set; }
    public ICommand ShowInfo { get; set; }
    public ICommand ShowAbout { get; set; }
    public ICommand Logout { get; set; }
    public ICommand Configuration { get; set; }
    public bool ConfigurationVisible { get; set; }

    private void ShowAuthenticationView()
    {
        AuthenticateVisible = !AuthenticateVisible;
        if (null != PropertyChanged)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("AuthenticateVisible"));
            PropertyChanged(this, new PropertyChangedEventArgs("Modal"));
        }
    }

    private void ShowConfigurationView()
    {
        ConfigurationVisible = !ConfigurationVisible;
        if (null != PropertyChanged)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("ConfigurationVisible"));
            PropertyChanged(this, new PropertyChangedEventArgs("Modal"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

UserControl Xaml

<UserControl x:Class="XXX.Connect"
             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:vm="clr-namespace:XXX.ViewModel"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <vm:ConnectViewModel x:Key="ViewModel"/>
        <vm:TwoWayVisibilityConverter x:Key="BoolToVisibility" />
    </UserControl.Resources>
    <Grid Width="280" 
            Height="173" 
            DataContext="{Binding Source={StaticResource ViewModel}}"
            Visibility="{Binding Path=Visible, Converter={StaticResource BoolToVisibility}, Mode=TwoWay}"
            Background="White">
        <Label Content="URL" Height="28" HorizontalAlignment="Left" Margin="12,12,0,0" Name="label1" VerticalAlignment="Top" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="102,12,0,0" Name="url" VerticalAlignment="Top" Width="169" Text="{Binding Path=Url, Mode=OneWayToSource}" TabIndex="0" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="102,70,0,0" Name="username" VerticalAlignment="Top" Width="171" Text="{Binding Path=Username, Mode=OneWayToSource}" TabIndex="2" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="102,41,0,0" Name="password" VerticalAlignment="Top" Width="169" Text="{Binding Path=Password, Mode=OneWayToSource}" TabIndex="1" />
        <Label Content="Username" Height="28" HorizontalAlignment="Left" Margin="12,39,0,0" Name="label3" VerticalAlignment="Top" />
        <Label Content="Password" Height="28" HorizontalAlignment="Left" Margin="12,68,0,0" Name="label2" VerticalAlignment="Top" />
        <DockPanel HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="13">
            <Button Content="OK" Height="23" HorizontalAlignment="Left"  Margin="5" Name="ok" VerticalAlignment="Top" Width="75" Command="{Binding ConnectCommand}" TabIndex="3" />
            <Button Content="Cancel" Height="23" HorizontalAlignment="Left" Margin="5" Name="cancel" VerticalAlignment="Top" Width="75" Command="{Binding CloseCommand}" TabIndex="4" />
        </DockPanel>
    </Grid>
</UserControl>

UserControl View Model

internal class ConnectViewModel : INotifyPropertyChanged
{
    [Import(typeof (IEventBus))] private IEventBus _bus;

    public ConnectViewModel()
    {
        ConnectCommand = new ForwardCommand(obj => Fire(),
                                            obj =>
                                            Visible && !String.IsNullOrEmpty(Url) && !String.IsNullOrEmpty(Url) &&
                                            !String.IsNullOrEmpty(Url));
        CloseCommand = new ForwardCommand(obj => Hide(), obj => Visible);
    }

    public ICommand ConnectCommand { get; set; }

    public ICommand CloseCommand { get; set; }

    public string Url { get; set; }

    public string Username { get; set; }

    public string Password { get; set; }

    private bool _visible;

    public bool Visible
    {
        get { return _visible; }
        set { _visible = value; }
    }

    private void Fire()
    {
        _bus.Publish<SessionCreatedEvent, SessionEventHandler>(new SessionCreatedEvent(Url, Username, Password));
        Hide();
    }

    private void Hide()
    {
        Visible = false;
        if (null != PropertyChanged)
            PropertyChanged(this, new PropertyChangedEventArgs("Visible")); 
    }

    public event PropertyChangedEventHandler PropertyChanged;
}
1
Please have a look at the markdown help... - H.B.
Thanks H.B. I have taken note. - user1310957

1 Answers

0
votes

Binding always works on properties. So while you Raise Visible in your Hide method, you don't raise it in the actual property. But the property is what the binding engine will set. If you bind this to another dependency property it won't get notified about it. Btw. whats a TwoWayVisibilityConverter? BooleanToVisibilityConverter is perfectly capable of handling two way bindings.

tl;dr to make two way binding work properly (and in fact even one way binding) you need to implement INotifyPropertyChanged properly which means, if the setter is called raise the property.

public bool Visible
{
    get { return _visible; }
    set 
    { 
        _visible = value; 
        if (null != PropertyChanged)
            PropertyChanged(this, new PropertyChangedEventArgs("Visible")); 
    }
}