【问题标题】:Dynamically updating a WPF Window before code finishes executing在代码完成执行之前动态更新 WPF 窗口
【发布时间】:2014-09-12 21:12:08
【问题描述】:

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

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

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

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

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

我想我在这里遗漏了一些非常基本的东西。如何让我的窗口立即加载,然后在任何属性发生更改时更新视图?我应该监听 PropertyChanged 事件然后更新视图吗?我在哪里做这个?在视图模型的设置器中?

下面是一些简化的代码:

从我的主窗口的视图模型调用我的窗口

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());
    }
}

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

【问题讨论】:

    标签: c# wpf mvvm


    【解决方案1】:

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

    用户能够感知到更新期间执行与之后执行之间的差异的唯一情况是,如果您在 UI 线程上有一个长时间运行的计算.不要那样做。

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

    【讨论】:

      【解决方案2】:

      我会做什么:

      不要在 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 方法的情况下绘制窗口。同步任务完成后,将从 SyncResult 填充视图模型属性,并且绑定将在屏幕上绘制它们。

      希望您能理解,如果我的代码中有一些错误,很抱歉,不确定它是否全部编译。

      【讨论】:

      • 我的问题的所有答案都非常有帮助,让我增加了我的理解,但是这个给了我最大的开端,我现在已经成功地实现了类似的东西 a) 像我一样工作'd 的意图和 b) 更易于维护。谢谢@jure
      【解决方案3】:

      首先,确保将 viewmodel 设置为视图的 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());
      }
      

      【讨论】:

        【解决方案4】:

        通过数据绑定,只要通知绑定到的属性已更改,您的 Window 就会自动更新。因此,您需要在视图模型中实现 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
        }
        

        【讨论】:

        • 感谢关于如何实现 INotifyPropertyChanged 的​​非常简洁的解释,非常有帮助,我就快到了,但不确定如何触发更改的属性。我现在需要实施与“异步”相关的建议以立即打开我的窗口。
        【解决方案5】:

        如果其他人在 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);
           }
        }
        

        然后,只需使用 as:

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

        引用原作者:

        Refresh 方法是一种扩展方法,它采用任何 UI 元素,然后调用该 UIElement 的 Dispatcher 的 Invoke 方法。 诀窍是调用带有 Render 或更低的 DispatcherPriority 的 Invoke 方法。因为我们不想做任何事情,所以我创建了一个空委托。那么这怎么实现刷新功能呢?

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-08-10
          • 2012-06-26
          相关资源
          最近更新 更多