在代码完成执行之前动态更新WPF窗口



这可能是一个非常基本的问题,请耐心等待,我对WPF/C#的世界还很陌生。

我有一个WPF应用程序,如果点击按钮,我会打开一个新窗口。

该窗口名为Sync,它所做的只是实例化一个视图模型类,该类包含一些绑定到我的视图的公共属性。

viewmodel还实例化了一个包含大量业务逻辑的类,这会更新viewmodel的绑定属性,目的是更新我的窗口的内容。

这种方法是可行的,但只有当所有(有时相当长的)处理完成时,才会加载窗口,并且视图会填充ViewModel属性的最后一个值。

我想我错过了一些基本的东西。如何让我的窗口立即加载,然后在任何属性更改时更新视图?我应该先侦听PropertyChanged事件,然后更新视图吗?我在哪里做这个?在视图模型的setter中?

这里有一些简化的代码:

从主窗口的View Model 调用我的窗口

public void SyncAction()
{
Sync syncWindow = new Sync();
syncWindow.Show();
syncWindow.Activate();
}

窗口

public partial class Sync : Window
{
public Sync()
{
InitializeComponent();
var viewModel = new SyncViewModel();
}
}

视图模型

class SyncViewModel
{
private string _miscStatus = "";
public SyncViewModel()
{
var sync = new SyncLogic();
sync.SyncAll(this);
}
public string MiscStatus
{
get
{
return _miscStatus;
}
set
{
_miscStatus += value;
}
}
}

一些业务逻辑

class SyncLogic
{
private ViewModel.SyncViewModel _syncViewModel;
public void SyncAll(ViewModel.SyncViewModel syncViewModel)
{
_syncViewModel = syncViewModel;
// lock our synctime
var syncTime = DateTools.getNow();
_syncViewModel.MiscStatus = "Sync starting at " + syncTime.ToString();
// Do lots of other stuff
_syncViewModel.MiscStatus = String.Format("Sync finished at at {0}, total time taken {1}", 
DateTools.getNow().ToString(), (DateTools.getNow() - syncTime).ToString());
}
}

额外的问题:我从业务逻辑中更新视图的方式(通过传递对视图模型的引用并从中更新其属性)似乎有点笨拙。我当然想将业务逻辑分开,但不确定如何将任何输出传递回视图模型。请问有什么更好的方法呢?

为什么关心更新是在代码执行之前还是之后产生视觉效果?内部属性会立即更新;任何查询UI的代码都将看到新的值。

如果用户在UI线程上有长时间运行的计算,那么用户唯一能够感知执行期间的更新与执行后的更新<em之间的差异。不要那样做。>

相反,与UI异步运行计算,以便可以同时处理重新绘制消息。您可以使用后台线程来实现这一点,但使用C#4及更高版本的新的更简单的方法是async。因为async是使用到UI线程的延续消息实现的,所以不需要在线程之间同步数据访问或封送UI访问。它只是工作,而且非常好。您唯一需要做的就是将代码分成足够小的块,每个块都作为async方法实现,这样就不会造成明显的延迟。

我会做什么:

不要在ViewModel构造函数中执行任何繁重的逻辑。构造函数应该只初始化对象,而不执行其他操作。在您的示例中,构造函数应该为空。

public SyncViewModel()
{       
}

SyncLogic不应该知道ViewModel。引入一些其他类来交流输入参数和同步结果。比方说SyncArgumentsSyncResult

class SyncLogic
{
public SyncResult SyncAll(SyncArguments syncArgs)
{
var syncResult = new SyncResult();
// Do lots of other stuff
// populate syncResult
return syncResult;
}  
}

在视图模型中引入一个应该被调用来执行"同步"逻辑的方法,并使该方法成为async。这样就很容易在后台完成繁重的工作,并让UI线程完成它应该做的工作,绘制UI。

public async Task Sync()
{
// lock our synctime
var syncTime = DateTools.getNow();
MiscStatus = "Sync starting at " + syncTime.ToString();

var sync = new SyncLogic();
var syncArgs = new SyncArguments();
//populate syncArgs from ViewModel data
//call the SyncAll as new Task so it will be executed as background operation
//and "await" the result
var syncResults = await Task.Factory.StartNew(()=>sync.SyncAll(syncArgs));
//when the Task completes your execution will continue here and you can populate the   
//ViewModel with results
MiscStatus = String.Format("Sync finished at at {0}, total time taken {1}", 
DateTools.getNow().ToString(), (DateTools.getNow() - syncTime).ToString());
}

制作创建并显示窗口async的按钮点击事件处理程序,这样您就可以在ViewModel 上调用Sync方法

private void async Button_click(object sender, EventArgs e)
{
Sync syncWindow = new Sync(); 
var viewModel = new SyncViewModel();
syncWindow.DataContext = viewModel;
syncWindow.Show();
syncWindow.Activate();
await viewModel.Sync();
}

这将在不等待Sync方法的情况下绘制Window。同步taks完成后,视图模型属性将从SyncResult中填充,绑定将在屏幕上绘制它们。

希望你明白这个想法,如果我的代码中有一些错误,很抱歉,不确定是否全部编译完成。

首先,确保将视图模型设置为视图的DataContext:

public partial class Sync : Window
{
public Sync()
{
InitializeComponent();
var viewModel = new SyncViewModel();
DataContext = viewModel;
}
}

其次,您必须在后台线程上运行"同步"功能。使用.Net 4.5:中的async+await关键字,这是最简单的

public async void SyncAll(ViewModel.SyncViewModel syncViewModel)
{
_syncViewModel = syncViewModel;
// lock our synctime
var syncTime = DateTools.getNow();
_syncViewModel.MiscStatus = "Sync starting at " + syncTime.ToString();
await Task.Factory.StartNew(() => {
// Do lots of other stuff
});
_syncViewModel.MiscStatus = String.Format("Sync finished at at {0}, total time taken {1}", 
DateTools.getNow().ToString(), (DateTools.getNow() - syncTime).ToString());
}

使用数据绑定,只要窗口通知其绑定的属性已更改,它就会自动更新。因此,您需要在视图模型中实现INotifyPropertyChanged,并在绑定源属性值发生更改时引发属性更改事件。例如:

public class SyncViewModel : INotifyPropertyChanged
{
private string _miscStatus = "";
public string MiscStatus
{
get{ return _miscStatus; }
set
{
_miscStatus += value;
OnPropertyChanged("MiscStatus");
}
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}

如果其他人在WPF中遇到这个问题,这里描述的解决方案非常简单,对我来说效果很好。它使用了一个扩展方法来强制呈现UIElement:

public static class ExtensionMethods
{
private static Action EmptyDelegate = delegate() { };

public static void Refresh(this UIElement uiElement)
{
uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
}
}

然后,简单地用作:

private void SomeLongOperation()
{
// long operations...
// UI update
label1.Content = someValue;
label1.Refresh();
// continue long operations   
}
}

引用原作者:

Refresh方法是一种扩展方法,它接受任何UI元素,然后调用该UIElement的Dispatcher的Invoke方法诀窍是调用DispatcherPriority为Render或更低的Invoke方法。由于我们什么都不想做,我创建了一个空的委托。那么,这是如何实现刷新功能的呢?

当DispatcherPriority设置为Render(或更低)时,代码将执行该优先级或更高的所有操作。在该示例中,代码已经将label1.Content设置为其他内容,这将导致渲染操作因此,通过调用Dispatcher.Invoke,代码本质上要求系统执行所有具有Render或更高优先级的操作,因此控件将呈现自己(绘制新内容)。然后,它将执行提供的委托(这是我们的空方法)。

最新更新