问题
I have a UserControl with several TextBox controls and a ProgressBar. The TextBox controls properly reflect the properties in codebehind to which they are bound. The ProgressBar does not respond to property change, however.
My XAML:
<UserControl
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:Controls="clr-namespace:Cmc.Installer.Controls;assembly=Cmc.Installer.Controls" x:Class="Cmc.Installer.Modules.MobileRecruiter.MobileRecruiterModule"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800">
<Grid HorizontalAlignment="Left" Height="580" Margin="10,10,0,0" VerticalAlignment="Top" Width="780">
<Canvas>
<Label Content="Database Server" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
<TextBox Text="{Binding DatabaseServer}" Height="23" Canvas.Left="160" Canvas.Top="12" Width="160"/>
<Label Content="Database Name" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="38"/>
<TextBox Text="{Binding DatabaseName}" Height="23" Canvas.Left="160" Canvas.Top="40" Width="160"/>
<Label Content="Database Username" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="66"/>
<TextBox Text="{Binding DatabaseUsername}" Height="23" Canvas.Left="160" Canvas.Top="68" Width="160"/>
<Label Content="Database Password" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="94"/>
<Controls:BindablePasswordBox Password="{Binding DatabasePassword}" Height="23" Canvas.Left="160" Canvas.Top="96" Width="160"/>
<ProgressBar Name="ProgressBar" Value="{Binding Progress}" Minimum="0" Maximum="100" Canvas.Left="10" Canvas.Top="164" Width="760" Height="24" />
</Canvas>
</Grid>
</UserControl>
And its codebehind (very abbreviated):
public partial class MobileRecruiterModule : UserControl, INotifyPropertyChanged
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private int _progress;
public MobileRecruiterModule()
{
InitializeComponent();
DataContext = this;
}
public string DatabaseServer { get; set; }
public string DatabaseName { get; set; }
public string DatabaseUsername { get; set; }
public string DatabasePassword { get; set; }
public int Progress
{
get { return _progress; }
set
{
if (value == _progress) return;
_progress = value;
OnPropertyChanged("Progress");
Logger.Trace("Progress.set() = " + _progress);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
// This is called by an external class
public void OnProgressChanged(object sender, ProgressChangedEventArgs args)
{
Progress = args.ProgressPercentage;
}
}
I know the value of Progress
is changing because I see it in the NLog logs:
2014-04-17 16:22:54.4068|TRACE|Cmc.Installer.Modules.MobileRecruiter.MobileRecruiterModule|Progress.set() = 28
I don't understand why the ProgressBar doesn't update when I fire OnPropertyChanged
in the setter just before the logging call.
回答1:
I replicated a scaled down version of your app in an MVVM pattern and had good luck with it. I used this code to replicate your user control...
<UserControl x:Class="ProgressBarBinding.Login"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid HorizontalAlignment="Left" Height="580" Margin="10,10,0,0" VerticalAlignment="Top" Width="780">
<Canvas>
<Label Content="Database Server" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
<TextBox Text="{Binding DatabaseServer}" Height="23" Canvas.Left="160" Canvas.Top="12" Width="160"/>
<Label Content="Database Name" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="38"/>
<TextBox Text="{Binding DatabaseName}" Height="23" Canvas.Left="160" Canvas.Top="40" Width="160"/>
<Label Content="Database Username" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="66"/>
<TextBox Text="{Binding DatabaseUsername}" Height="23" Canvas.Left="160" Canvas.Top="68" Width="160"/>
<Label Content="Database Password" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="94"/>
<ProgressBar Name="ProgressBar" Value="{Binding Progress}" Minimum="0" Maximum="100" Canvas.Left="10" Canvas.Top="164" Width="760" Height="24" />
</Canvas>
</Grid>
</UserControl>
The only thing missing from that is your proprietary password control, which does not affect the solution.
I encoded this control into a MainWindow.xaml file thusly...
<Window x:Class="ProgressBarBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ProgressBarBinding"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<vm:ViewModel x:Key="ViewModel"/>
</Window.Resources>
<Grid DataContext="{StaticResource ViewModel}">
<vm:Login/>
</Grid>
</Window>
Note that the window resource definition includes a reference to a view model instance. Most people set up MVVM with dependency injection, but this approach is good for quick trials and Indicative Code
. The view model is set as the Grid's data context. Your control inherits the data context from the grid. That's the end of the xaml code. There is no code-behind in the MainWindow.xaml.cs file other than the call to InitializeComponent (and that's where the VM instance gets created).
The ViewModel class looks like this...
public class ViewModel : INotifyPropertyChanged
{
private readonly SynchronizationContext _synchronizationContext = SynchronizationContext.Current;
public ViewModel()
{
DatabaseServer = "AnyServer";
DatabaseName = "Any name";
Model m = new Model();
Task.Run(() => m.DoWork(this));
}
public string DatabaseServer { get; set; }
public string DatabaseName { get; set; }
public string DatabaseUsername { get; set; }
public string DatabasePassword { get; set; }
private int _progress;
public int Progress
{
get { return _progress; }
set
{
if (value == _progress) return;
_progress = value;
OnPropertyChanged("Progress");
Console.WriteLine(@"Progress.set() = " + _progress);
}
}
// This is called by an external class
public void OnProgressChanged(object sender, ProgressChangedEventArgs args)
{
_synchronizationContext.Send(delegate { Progress = args.ProgressPercentage; }, null);
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string name)
{
var handler = Interlocked.CompareExchange(ref PropertyChanged, null, null);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
Most of the code in the view model looks like yours except there are no dependencies on UI elements. Everything is done via binding. I used a SynchronizationContext in the callback, although it may not be necessary in your application.
The constructor of the VM starts a model on a TPL thread. The model looks like this...
public class Model
{
public void DoWork(ViewModel vm)
{
int progressPercentage = 0;
for (int i = 0; i < 100000; i++)
{
vm.OnProgressChanged(this, new ProgressChangedEventArgs(progressPercentage, null));
if (i%1000 == 0)
{
++progressPercentage;
}
}
}
}
So putting it all together, the model is running in its own thread, and the UI is being updated on its own thread. The whole thing works as expected.
The ProgressBar will increment its way up to 100 and the UI will remain responsive while the model is doing its work. This answer does not explain why your original code does not work, but I suspect it has to do with the UI thread being starved out. This is evidenced by your complete log history, but nothing changing on the UI. Overall, this answer moves toward what others have suggested in their commentary: namely that the MVVM approach of binding has a lot to offer.
回答2:
Since you're in a UserControl, you need to explicitly give it a name and use the ElementName tag when binding, like this:
<UserControl
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:Controls="clr-namespace:Cmc.Installer.Controls;assembly=Cmc.Installer.Controls" x:Class="Cmc.Installer.Modules.MobileRecruiter.MobileRecruiterModule"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800" x:Name="MyControl">
<Grid HorizontalAlignment="Left" Height="580" Margin="10,10,0,0" VerticalAlignment="Top" Width="780">
<Canvas>
<ProgressBar Name="ProgressBar" Value="{Binding Progress, ElementName=MyControl}" Minimum="0" Maximum="100" Canvas.Left="10" Canvas.Top="164" Width="760" Height="24" />
</Canvas>
</Grid>
</UserControl>
回答3:
Why do you need Binding
if you have the Actual control
. Since you are not doing it in MVVM
just call the ProgressBar
right away.
ProgressBar.Dispatcher.Invoke(() => ProgressBar.Value = Progress = args.ProgressPercentage);
Sorry but I don't see the benefit of the Binding
if all the properties/controls are accessible in your View
's class.
Binding is more useful and powerful if you implemented MVVM
.
来源:https://stackoverflow.com/questions/23142897/wpf-progressbar-not-updating-with-inotifypropertychanged