WPF 双向绑定不起作用 [英] WPF two-way binding not working

查看:35
本文介绍了WPF 双向绑定不起作用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个分配给我的主窗口的数据上下文 (UserPreferences),以及一个文本框,该文本框将双向绑定到数据上下文属性之一中的属性 (CollectionDevice>) 在上下文中.

I have a data context (UserPreferences) assigned to my main window, and a textbox that binds two-way to a property within one of the data context's properties (CollectionDevice) within the context.

当窗口加载时,文本框不会绑定到我的模型中的属性.我在调试器中验证数据上下文是否设置为模型对象并且模型的属性已正确分配.然而,我得到的只是一系列带有 0 的文本框.

When the Window loads, the textbox's do not bind to the properties in my model. I verify within the debugger that the data context is set to the model object and the model's properties are properly assigned. All I get however are a series of textbox's with 0's in them.

当我将数据输入文本框时,模型中的数据会更新.问题只是在我加载数据并将其应用于数据上下文时发生,文本框没有更新.

When I enter the data into the textbox's, the data is updated in the model. The issue just happens when I load the data and apply it to the data context, the text box does not get updated.

当我将模型保存到数据库时,会从文本框中保存正确的数据.当我从数据库中恢复模型时,会应用正确的数据.当模型应用于我的构造函数中的数据上下文时,文本框的数据上下文包含正确的数据,并且它的属性按应有的方式分配.问题是 UI 没有反映这一点.

When I save the model to the database, the proper data gets saved from the textbox. When I restore the model from the database, the proper data gets applied. When the model is applied to the data context within my constructor, the textbox's datacontext contains the correct data and it's properties are assigned as they should be. The issue is the UI does not reflect this.

XAML

<Window.DataContext>
    <models:UserPreferences />
</Window.DataContext>

        <!-- Wrap pannel used to store the manual settings for a collection device. -->
        <StackPanel Name="OtherCollectionDevicePanel">
            <StackPanel Orientation="Horizontal">
                <TextBlock VerticalAlignment="Center" Margin="10, 10, 0, 0" Text="Baud Rate" />
                <TextBox Name="BaudRateTextBox" Text="{Binding Path=SelectedCollectionDevice.BaudRate, Mode=TwoWay}" Margin="10, 10, 0, 0" MinWidth="80" ></TextBox>
            </StackPanel>
            <WrapPanel>
                <TextBlock VerticalAlignment="Center" Margin="10, 10, 0, 0" Text="Com Port" />
                <TextBox Text="{Binding Path=SelectedCollectionDevice.ComPort, Mode=TwoWay}" Margin="10, 10, 0, 0" MinWidth="80" ></TextBox>
            </WrapPanel>
            <WrapPanel>
                <TextBlock VerticalAlignment="Center" Margin="10, 10, 0, 0" Text="Data Points" />
                <TextBox Text="{Binding Path=SelectedCollectionDevice.DataPoints, Mode=TwoWay}" Margin="10, 10, 0, 0" MinWidth="80" ></TextBox>
            </WrapPanel>
            <WrapPanel Orientation="Horizontal">
                <TextBlock VerticalAlignment="Center" Margin="10, 10, 0, 0" Text="WAAS" />
                <CheckBox IsChecked="{Binding Path=SelectedCollectionDevice.WAAS, Mode=TwoWay}" Content="Enabled" Margin="20, 0, 0, 0" VerticalAlignment="Bottom"></CheckBox>
            </WrapPanel>
        </StackPanel>

模型 <-- 数据上下文.

Model <-- Datacontext.

/// <summary>
/// Provides a series of user preferences.
/// </summary>
[Serializable]
public class UserPreferences : INotifyPropertyChanged
{
    private CollectionDevice selectedCollectionDevice;

    public UserPreferences()
    {
        this.AvailableCollectionDevices = new List<CollectionDevice>();

        var yuma1 = new CollectionDevice
        {
            BaudRate = 4800,
            ComPort = 31,
            DataPoints = 1,
            Name = "Trimble Yuma 1",
            WAAS = true
        };

        var yuma2 = new CollectionDevice
        {
            BaudRate = 4800,
            ComPort = 3,
            DataPoints = 1,
            Name = "Trimble Yuma 2",
            WAAS = true
        };

        var toughbook = new CollectionDevice
        {
            BaudRate = 4800,
            ComPort = 3,
            DataPoints = 1,
            Name = "Panasonic Toughbook",
            WAAS = true
        };


        var other = new CollectionDevice
        {
            BaudRate = 0,
            ComPort = 0,
            DataPoints = 0,
            Name = "Other",
            WAAS = false
        };

        this.AvailableCollectionDevices.Add(yuma1);
        this.AvailableCollectionDevices.Add(yuma2);
        this.AvailableCollectionDevices.Add(toughbook);
        this.AvailableCollectionDevices.Add(other);

        this.SelectedCollectionDevice = this.AvailableCollectionDevices.First();
    }

    /// <summary>
    /// Gets or sets the GPS collection device.
    /// </summary>
    public CollectionDevice SelectedCollectionDevice
    {
        get
        {
            return selectedCollectionDevice;
        }
        set
        {
            selectedCollectionDevice = value;
            this.OnPropertyChanged("SelectedCollectionDevice");
        }
    }

    /// <summary>
    /// Gets or sets a collection of devices that can be used for collecting GPS data.
    /// </summary>
    [Ignore]
    [XmlIgnore]
    public List<CollectionDevice> AvailableCollectionDevices { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Notifies objects registered to receive this event that a property value has changed.
    /// </summary>
    /// <param name="propertyName">The name of the property that was changed.</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

CollectionDevice <-- 文本框绑定到的位置.

CollectionDevice <-- Where text box binds to.

/// <summary>
/// CollectionDevice model
/// </summary>
[Serializable]
public class CollectionDevice : INotifyPropertyChanged
{
    /// <summary>
    /// Gets or sets the COM port.
    /// </summary>
    private int comPort;

    /// <summary>
    /// Gets or sets a value indicating whether [waas].
    /// </summary>
    private bool waas;

    /// <summary>
    /// Gets or sets the data points.
    /// </summary>
    private int dataPoints;

    /// <summary>
    /// Gets or sets the baud rate.
    /// </summary>
    private int baudRate;

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Gets or sets the COM port.
    /// </summary>
    public int ComPort
    {
        get
        {
            return this.comPort;
        }

        set
        {
            this.comPort= value;
            this.OnPropertyChanged("ComPort");
        }
    }

    /// <summary>
    /// Gets or sets the COM port.
    /// </summary>
    public bool WAAS
    {
        get
        {
            return this.waas;
        }

        set
        {
            this.waas = value;
            this.OnPropertyChanged("WAAS");
        }
    }

    /// <summary>
    /// Gets or sets the COM port.
    /// </summary>
    public int DataPoints
    {
        get
        {
            return this.dataPoints;
        }

        set
        {
            this.dataPoints = value;
            this.OnPropertyChanged("DataPoints");
        }
    }

    /// <summary>
    /// Gets or sets the COM port.
    /// </summary>
    public int BaudRate
    {
        get
        {
            return this.baudRate;
        }

        set
        {
            this.baudRate = value;
            this.OnPropertyChanged("BaudRate");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Notifies objects registered to receive this event that a property value has changed.
    /// </summary>
    /// <param name="propertyName">The name of the property that was changed.</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public override string ToString()
    {
        return this.Name;
    }
}

有人能指出我正确的方向吗?我认为问题是我在 XAML 中的绑定;不过我找不到.我需要它是双向绑定的,因为数据可以在模型内的应用程序生命周期内随时更改(数据库通过同步更新)并且 UI 需要反映这些更改,但用户可以通过以下方式将更改应用于模型用户界面.

Can someone point me in the right direction? I assume the issue is my binding in XAML; I can't find it though. I need it to be two-way bound because the data can change at any time during the apps lifetime within the model (database is updated through syncs) and the UI needs to reflect those changes, yet the user can apply changes to the model via the UI.

更新 1

我尝试强制更新文本框数据绑定,但效果不佳.

I tried to force the text box databind to be updated, but that did not work as well.

BindingExpression be = this.BaudRateTextBox.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();

我也尝试将 UpdateSourceTrigger 设置为 PropertyChanged 并且这似乎也没有解决问题.

I also tried setting the UpdateSourceTrigger to PropertyChanged and that did not seem to resolve the problem either.

<TextBox Name="BaudRateTextBox" Text="{Binding Path=SelectedCollectionDevice.BaudRate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="10, 10, 0, 0" MinWidth="80" ></TextBox>

更新 2

我试图遵循一些文档微软,它似乎没有解决这个问题.当窗口加载时,这些值仍然保持为 0.从数据库恢复对象的状态后,绑定不会更新.绑定已连接,因为当我输入数据时,数据上下文会更新.出于某种原因,当我将它设置为双向时,它就像单向一样.

I tried to follow along with some documentation from Microsoft and it does not seem to fix the issue. The values still remain 0 when the window loads. The binding is not being updated after I restore the state of the object from the database. Binding is wired up though because as I enter data, the data context is updated. For some reason, it's acting like One-Way when I have it set to Two-Way.

更新 3

我试图将代码移入窗口加载事件并移出构​​造函数,但这似乎没有帮助.我发现有趣的是,在反序列化过程中不会触发 PropertyChanged 事件.我认为在这种情况下这并不重要,因为对象已完全恢复正确,然后我只是将其分配给数据上下文.我将数据上下文从 XAML 移到 WindowLoaded 中,以测试 XAML 是否是问题所在.结果是一样的.

I tried to move the code into the window loaded event and out of the constructor but that did not appear to help. Something I found interesting is that the PropertyChanged event does not get fired during the deserialization process. I don't think it matters in this case because the object is fully restored properly and then I just assign it to the data context anyway. I moved the data context out of the XAML and into the WindowLoaded in order to test if the XAML was the problem. The result was the same.

private void WindowLoaded(object sender, RoutedEventArgs e)
{
    // Restore our preferences state.
    var preferences = new UserPreferenceCommands();
    Models.UserPreferences viewModel = new Models.UserPreferences();

    // Set up the event handler before we deserialize.
    viewModel.PropertyChanged += viewModel_PropertyChanged;
    preferences.LoadPreferencesCommand.Execute(viewModel);

    // At this point, viewModel is a valid object. All properties are set correctly.
    viewModel = preferences.Results;

    // After this step, the UI still shows 0's in all of the text boxs. Even though the values are not zero.
    this.DataContext = viewModel;
}

// NEVER gets fired from within the WindowLoaded event.
void viewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    MessageBox.Show("Property changed!");
}

// This changes the model properties and is immediately reflected in the UI. Why does this not happen within the WindowLoaded event?
private void TestButtonClickEvent(object sender, RoutedEventArgs e)
{
    var context = this.DataContext as Models.UserPreferences;
    context.SelectedCollectionDevice.ComPort = 1536;
}

更新 4 - 发现问题

我已经确定了问题,但仍需要解决方案.数据绑定的全部意义在于我不必执行此手动分配.我的 INotify 实现有问题吗?

I have identified the problem, but still need a resolution. The whole point of data binding is so that I don't have to perform this manual assignment. Is there something wrong with my INotify implementations?

private void WindowLoaded(object sender, RoutedEventArgs e)
{
    // Restore our preferences state.
    var preferences = new UserPreferenceCommands();
    Models.UserPreferences viewModel = new Models.UserPreferences();

    // Set up the event handler before we deserialize.
    viewModel.PropertyChanged += viewModel_PropertyChanged;
    preferences.LoadPreferencesCommand.Execute(viewModel);

    // At this point, viewModel is a valid object. All properties are set correctly.
    viewModel = preferences.Results;

    // After this step, the UI still shows 0's in all of the text boxs. Even though the values are not zero.
    this.DataContext = viewModel;

    // SOLUTION: - Setting the actual property causes the UI to be reflected when the window is initialized; setting the actual data context does not. Why? Also note that I set this property and my PropertyChanged event handler still does not fire.
    ((Models.UserPreferences) DataContext).SelectedCollectionDevice = viewModel.SelectedCollectionDevice;

}

推荐答案

好的,我能够确定问题并解决它.事实证明这是导致这种情况的事情的汇编.

Alright, I was able to determine the problem and get it resolved. It turned out to be a compilation of things causing this.

首先,我的模型.

UserPreferences <-- MainWindow 是绑定到此的数据.

UserPreferences <-- MainWindow is data bound to this.

[Serializable]
public class UserPreferences : INotifyPropertyChanged
{
    private CollectionDevice selectedCollectionDevice;

    public UserPreferences()
    {
        this.AvailableCollectionDevices = new List<CollectionDevice>();

        var yuma1 = new CollectionDevice
        {
            BaudRate = 4800,
            ComPort = 31,
            DataPoints = 1,
            Name = "Trimble Yuma 1",
            WAAS = true
        };

        var yuma2 = new CollectionDevice
        {
            BaudRate = 4800,
            ComPort = 3,
            DataPoints = 1,
            Name = "Trimble Yuma 2",
            WAAS = true
        };

        var toughbook = new CollectionDevice
        {
            BaudRate = 4800,
            ComPort = 3,
            DataPoints = 1,
            Name = "Panasonic Toughbook",
            WAAS = true
        };


        var other = new CollectionDevice
        {
            BaudRate = 0,
            ComPort = 0,
            DataPoints = 0,
            Name = "Other",
            WAAS = false
        };

        this.AvailableCollectionDevices.Add(yuma1);
        this.AvailableCollectionDevices.Add(yuma2);
        this.AvailableCollectionDevices.Add(toughbook);
        this.AvailableCollectionDevices.Add(other);

        this.SelectedCollectionDevice = this.AvailableCollectionDevices.First();
    }

    /// <summary>
    /// Gets or sets the GPS collection device.
    /// </summary>
    public CollectionDevice SelectedCollectionDevice
    {
        get
        {
            return selectedCollectionDevice;
        }
        set
        {
            selectedCollectionDevice = value;

            if (selectedCollectionDevice.Name == "Other")
            {
                this.AvailableCollectionDevices[3] = value;
            }

            this.OnPropertyChanged("SelectedCollectionDevice");
        }
    }

    /// <summary>
    /// Gets or sets a collection of devices that can be used for collecting GPS data.
    /// </summary>
    [Ignore]
    [XmlIgnore]
    public List<CollectionDevice> AvailableCollectionDevices { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Notifies objects registered to receive this event that a property value has changed.
    /// </summary>
    /// <param name="propertyName">The name of the property that was changed.</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

SelectedCollectionDevice 的 setter 中,我不想查看所选设备是否是 other.所有其他设备(yuma1、panasonic 等)都具有永不更改的预先确定的属性值.当用户选择其他"时,将显示文本框,他们可以手动输入数据.问题是,在窗口加载过程中从数据库恢复手动输入的数据时,我没有将SelectedCollectionDevice中的自定义数据分配给集合中的相应对象.

In the setter for the SelectedCollectionDevice I was not looking to see if the selected device was other. All of the other devices (yuma1, panasonic etc) have pre-determined property values that are never changed. When the user selects "Other" the textbox's are displayed and they can manually enter the data. The problem was that when the manually entered data was restored from the database during the window loading, I was not assigning the custom data in SelectedCollectionDevice to the corresponding object in the collection.

在窗口加载期间,Combobox.SelectedItem 被设置为 SelectedCollectionDevice 的索引.Combobox.ItemsSource 被设置为 AvailableCollectionDevices 集合.

During window load, the Combobox.SelectedItem was set to the index of the SelectedCollectionDevice. The Combobox.ItemsSource was set to the AvailableCollectionDevices collection.

this.CollectionDevice.SelectedIndex = 
    viewModel.AvailableCollectionDevices.IndexOf(
        viewModel.AvailableCollectionDevices.FirstOrDefault(
            acd => acd.Name == viewModel.SelectedCollectionDevice.Name));

执行上述代码时,组合框从其数据源中提取默认对象,该对象的所有值都设置为零.在组合框的 SelectionChanged 事件中,我将数据上下文 SelectedCollectionDevice 分配给与组合框关联的归零项.

When the above code is executed, the combo box pulls the default object from its data source, which has all of the values set to zero. Within the combo box's SelectionChanged event I assigned the Data Context SelectedCollectionDevice to the zero'd out item associated with the combo box.

private void CollectionDeviceSelected(object sender, SelectionChangedEventArgs e)
{
    if (e.AddedItems.Count > 0 && e.AddedItems[0] is CollectionDevice)
    {
        // Assign the view models SelectedCollectionDevice to the device selected in the combo box.
        var device = e.AddedItems[0] as CollectionDevice;
        ((Models.UserPreferences)this.DataContext).SelectedCollectionDevice = device;

        // Check if Other is selected. If so, we have to present additional options.
        if (device.Name == "Other")
        {
            OtherCollectionDevicePanel.Visibility = Visibility.Visible;
        }
        else if (OtherCollectionDevicePanel.Visibility == Visibility.Visible)
        {
            OtherCollectionDevicePanel.Visibility = Visibility.Collapsed;
        }
    }
}

长话短说,我在 SelectedCollectionDevice 的 setter 中添加了上面的代码,以将值应用到 AvailableCollectionDevices List<>.这样,当组合框选择了其他"值时,它会从具有正确数据的集合中提取值.在反序列化过程中,我只是反序列化了 SelectedCollectionDevice 而不是 List<> 这就是为什么在第一次加载窗口时数据总是被覆盖的原因.

So long story short, I added the code above in the setter for the SelectedCollectionDevice to apply the value to the AvailableCollectionDevices List<>. This way, when the combo box has the "Other" value selected, it pulls the value from the collection with the correct data. During deserialization, I am just deserializing the SelectedCollectionDevice and not the List<> which is why the data was always being overwrote when the window first loaded.

这也解释了为什么使用本地 viewModel.SelectedCollectionDevice 重新分配数据上下文 SelectedCollectionDevice 属性是有效的.我正在替换与组合框关联的清零对象,该对象在 SelectionChanged 事件期间设置了数据上下文.我无法在 XAML 中设置 DataContext 并删除手动分配.

This also explains why re-assigning the the data context SelectedCollectionDevice property with the local viewModel.SelectedCollectionDevice was working. I was replacing the zero'd out object associated with the combo box, which had set the data context during the SelectionChanged event. I am not able to set the DataContext in the XAML and remove the manual assignment.

感谢所有的帮助,它帮助我缩小了调试范围,直到我最终解决了问题.非常感谢!

Thanks for all of the help, it helped me narrow down my debugging until I finally resolved the issue. Much appreciated!

这篇关于WPF 双向绑定不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆