这可能是一个非常基本的问题,请耐心等待,我对WPF / C#的世界还很陌生。
我有一个WPF应用程序,如果单击一个按钮,我打开一个新窗口。
该窗口称为Sync,它所做的只是实例化一个viewmodel类,它包含一些绑定到我视图的公共属性。
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());
}
}
奖金问题:我从业务逻辑中更新视图的方式(通过传递对viewmodel的引用并从那里更新其属性)似乎有些愚蠢。我当然希望保持业务逻辑分离,但我不确定如何将任何输出传递回viewmodel。请问这是一个更好的方法吗?
答案 0 :(得分:2)
为什么关心在代码完成执行之前或之后更新是否具有视觉效果?内部属性立即更新;任何查询UI的代码都会看到新值。
用户能够在执行期间感知更新与执行之后之间的区别时,唯一一次是在UI线程上有长时间运行的计算。不要那样做。
相反,与UI异步运行计算,以便同时处理重绘消息。您可以使用后台线程执行此操作,但C#4及更高版本的新更简单方法是async
。因为async
是使用UI线程的延续消息实现的,所以您不需要在线程之间同步数据访问或编组UI访问。它只是有效,而且非常好。您唯一需要做的就是将代码分成足够小的块,每个块都以async
方法实现,不会导致明显的延迟。
答案 1 :(得分:1)
首先,确保将viewmodel设置为视图的DataContext:
public partial class Sync : Window
{
public Sync()
{
InitializeComponent();
var viewModel = new SyncViewModel();
DataContext = viewModel;
}
}
其次,你必须在后台线程上运行“sync”内容。使用.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());
}
答案 2 :(得分:1)
使用数据绑定,只要窗口通知它绑定的属性已更改,Window就会自动更新。因此,您需要在viewmodel中实现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
}
答案 3 :(得分:1)
我会做什么:
不要在ViewModel构造函数中执行任何繁重的逻辑。构造函数应该只初始化对象而不执行任何其他操作。在您的示例中,构造函数应为空。
public SyncViewModel()
{
}
SyncLogic
不应该知道ViewModel。引入一些其他类来传递输入参数和同步结果。我们说SyncArguments
和SyncResult
。
class SyncLogic
{
public SyncResult SyncAll(SyncArguments syncArgs)
{
var syncResult = new SyncResult();
// Do lots of other stuff
// populate syncResult
return syncResult;
}
}
在viewmodel中引入一个方法,该方法应该被调用以执行“sync”逻辑,并使该方法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方法。当Sync taks完成时,将从SyncResult填充viewmodel属性,Bindings将在屏幕上绘制它们。
希望你明白这个想法,对不起,如果我的代码中有一些错误,不确定它是否全部编译。
答案 4 :(得分:1)
如果其他人在WPF中遇到此问题,那么here描述的解决方案非常简单,对我来说工作正常。它使用扩展方法强制呈现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方法。 诀窍是使用DisdercherPriority为Render或更低版本来调用Invoke方法。由于我们不想做任何事情,我创建了一个空委托。那么为什么这会实现刷新功能?
当DispatcherPriority设置为Render(或更低)时,代码将执行具有该优先级或更高优先级的所有操作。在该示例中,代码已经将label1.Content设置为其他内容,这将导致渲染操作。 因此,通过调用Dispatcher.Invoke,代码实质上要求系统执行Render或更高优先级的所有操作,因此控件将自行呈现(绘制新内容)。然后,它将执行提供的委托(这是我们的空方法)。