WPF MVVM:视图加载后执行命令

22

我有一个基于WPF和MVVM的项目。 我的项目基于一个向导,其中包含一个内容控件,显示我的视图(用户控件)。 我希望在视图完全加载后执行命令,让用户在命令执行后立即看到视图界面。

我尝试使用:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Loaded">
        <i:InvokeCommandAction Command="{Binding StartProgressCommand}"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

但是命令在我看到视图界面之前就执行了,这不是我想要的。

有人有想法应该如何实现吗?

8个回答

18

这似乎比被接受的答案更为简洁。如果你只需要运行一次,为什么要启动一个计时器呢? - Graham Bass
3
对我来说,Invoke似乎行不通,所以我使用了BeginInvoke代替Invoke。这样做完美地解决了问题。 - Zohar Peled

17

这是因为即使技术上视图已经加载了(也就是说,所有的组件都已经准备好了),但你的应用程序还没有空闲,因此UI尚未刷新。

在Loaded事件上使用交互触发器设置命令已经很好了,因为没有更好的事件可供附加。
现在要真正等到UI显示出来,请在你的StartProgress()方法中执行以下操作(在这里假设这是StartProgressCommand指向的方法名称):

public void StartProgress()
{
    new DispatcherTimer(//It will not wait after the application is idle.
                       TimeSpan.Zero,
                       //It will wait until the application is idle
                       DispatcherPriority.ApplicationIdle, 
                       //It will call this when the app is idle
                       dispatcherTimer_Tick, 
                       //On the UI thread
                       Application.Current.Dispatcher); 
}

private static void dispatcherTimer_Tick(object sender, EventArgs e)
{
    //Now the UI is really shown, do your computations
}

3
对于急于结束线程的人,以下是如何结束线程的方法:在事件处理程序中,var timer = sender as DispatcherTimer; timer.Stop(); - Shawn
我不确定这是否是一个非常好的方法,但它确实有效! - peter70

2

另一种做法:

在你的用户控件XAML中定义这个xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"xmlns:mi="http://schemas.microsoft.com/expression/2010/interactions",并在你的项目中添加Microsoft.Expression.Interactions程序集。在你的触发器上使用CallMethodAction,就像下面这样:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Loaded">
        <mi:CallMethodAction TargetObject="{Binding}" MethodName="StartProgressCommand"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

将触发器放置在您的用户控件的根元素内,例如:grid。并且将您的ViewModel类中的StartProgressCommand从命令更改为普通的常规方法,例如:

public void StartProgressCommand()
{
  /* put your program logic here*/
}

每次用户控件被渲染时,它将精确地运行该方法一次。

简单而精确...只需编辑缺失的 xmlns:i 即可。 - andrepaulo
1
这对我不起作用,它告诉我该方法或操作未实现。 - Amen Jlili
每次渲染视图时都会调用加载事件。当将视图添加到此父视图中时,这可能会导致问题。 - DirtyNative

1
我们使用了一个计时器解决方案 - 我也对此非常怀疑,但它似乎运行良好。
public static class DispatcherExtensions
{
    private static Dictionary<string, DispatcherTimer> timers =
        new Dictionary<string, DispatcherTimer>();
    private static readonly object syncRoot = new object();

    public static void DelayInvoke(this Dispatcher dispatcher, string namedInvocation,
        Action action, TimeSpan delay,
        DispatcherPriority priority = DispatcherPriority.Normal)
    {
        lock (syncRoot)
        {
            RemoveTimer(namedInvocation);
            var timer = new DispatcherTimer(delay, priority, (s, e) =>
                {
                    RemoveTimer(namedInvocation);
                    action();
                }, dispatcher);
            timer.Start();
            timers.Add(namedInvocation, timer);
        }
    }


    public static void CancelNamedInvocation(this Dispatcher dispatcher, string namedInvocation)
    {
        lock (syncRoot)
        {
            RemoveTimer(namedInvocation);
        }
    }

    private static void RemoveTimer(string namedInvocation)
    {
        if (!timers.ContainsKey(namedInvocation)) return;
        timers[namedInvocation].Stop();
        timers.Remove(namedInvocation);
    } 


} 

然后我们使用调用

Dispatcher.CurrentDispatcher.DelayInvoke("InitSomething",()=> {
    DoSomething();
},TimeSpan.FromSeconds(1));

0

我用这个解决方案来解决这个问题。 我想使用一个布尔属性,开始时设置为true,在结束时设置为false,以便通知用户后台工作。

基本上,它使用

  • DispatcherTimer 在UI渲染后启动方法,根据this执行
  • 一个异步方法,将执行作为参数传递的Action

调用:

this.LaunchThisWhenUiLoaded(() => { /*Stuff to do after Ui loaded here*/ });

方法:

private DispatcherTimer dispatchTimer;
private Action ActionToExecuteWhenUiLoaded;

/// <summary>
/// Handy method to launch an Action after full UI rendering
/// </summary>
/// <param name="toExec"></param>
protected void LaunchThisWhenUiLoaded(Action toExec)
{            
    ActionToExecuteWhenUiLoaded = toExec;
    // Call UiLoaded method when UI is loaded and rendered
    dispatchTimer = new DispatcherTimer(TimeSpan.Zero, DispatcherPriority.ContextIdle, UiLoaded, Application.Current.Dispatcher);
}

/// <summary>
/// Method called after UI rendered
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected async void UiLoaded(object sender, EventArgs e)
{
    this.IsBusy = true;    

    if (ActionToExecuteWhenUiLoaded != null)
        await Task.Run(ActionToExecuteWhenUiLoaded);
    dispatchTimer.Stop();

    this.IsBusy = false;
}

可能不是很干净,但它按预期工作。


0
您可以检查视图的IsLoaded属性,当视图处于未加载状态时返回false,当视图完全加载时,该属性变为true。
谢谢, Rajnikant

1
它不起作用。在视图呈现之前,isLoaded就已经获得了true值。 - Ofir

0

你尝试过绑定到ContentRendered事件吗? 它会在已加载的事件之后发生,但我不确定这是否保证了UI线程完成了窗口的绘制。


2
我使用用户控件而不是窗口,因此事件ContentRendered不存在。 - Ofir
你的控件是动态添加到窗口中的吗?如果不是,那就绑定到父窗口的事件上。 - Dominik

0

你可以在"CommandExecute"方法的第一行写上"Thread.Sleep(10000)"。使用相同的加载触发器。

如果你不想使用Thread.Sleep,那么你可以选择使用"DispatcherTimer"。在你的命令执行方法中启动一个计时器,并将所有代码移到计时器的tick事件中。 将计时器间隔设置为2秒,这样用户就能看到UI。


我考虑过这个问题,但如果可能的话,我更倾向于避免基于时间假设执行命令。 - Ofir
虽然我不喜欢在我的应用程序中使用计时器,但是我认为在WPF中没有任何事件会在加载后的几秒钟后触发。如果您想在执行命令之前显示UI一段时间,则必须使用计时器。 - Bathineni
同意Offir的观点。通常情况下,不要通过时间假设来解决竞态条件问题。像Louis Kauffman所展示的那样,在Application.Idle上触发是最好的方法。 - blearyeye

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接