wpf mvvm 弹框之等待框
目录
一、效果
二、弹框主体改造
三、等待动画用户控件
四、弹窗 viewmodel 和帮助类的改造
五、使用方法和代码地址
独立观察员 2020年10月13日
之前写过一篇《wpf mvvm 模式下的弹窗》,里面实现了确认框和消息框,经过一段时间的演化,目前又新增了可显示自定义内容的弹框、可进行信息录入的弹框、以及本文将要介绍的加载等待框。
一、效果
先来看看效果,首先是其它弹框(动图):
然后是等待弹框(动图):
下面来看如何实现,当然,是在之前的基础上进行的,前一篇文章没看的话,需要先看一下,或者直接获取文末提供的代码查看。
二、弹框主体改造
首先改造的是,给右上角的 x 和底下的确认取消按钮区域的是否显示特性 visibility 绑定了相关属性,可以控制是否显示,这样在消息框情况下可以隐藏底部按钮,在等待框情况下可以都隐藏掉。
然后是中间的主体区域,图上看不出什么变化,实际上变化还是比较大的,代码如下:
文字版:
<scrollviewer grid.row="2" horizontalscrollbarvisibility="disabled" verticalscrollbarvisibility="auto">
<stackpanel margin="5" verticalalignment="center">
<textblock fontsize="16" text="{binding dialogmessage, fallbackvalue='是否确认操作?是否确认操作?是否确认操作?是否确认操作?是否确认操作?', targetnullvalue='是否确认操作?'}" textwrapping="wrap"
verticalalignment="center" horizontalalignment="center" visibility="{binding isshowtext, converter={staticresource visibleconverter}, fallbackvalue=visible}">
</textblock>
<contentcontrol visibility="{binding isshowcustom, converter={staticresource visibleconverter}, fallbackvalue=collapsed}" content="{binding customcontent}"
horizontalalignment="{binding customcontenthorizontalalignment, targetnullvalue=center, mode=oneway}" horizontalcontentalignment="center" minwidth="50">
</contentcontrol>
</stackpanel>
</scrollviewer>
最外层使用 scrollviewer 包裹,如果内容过多则可滚动。往里一层是 stackpanel,里面有一个 textblock 用于显示文本内容,还有一个 contentcontrol 用于显示自定义内容(绑定一个 frameworkelement 类型的对象)。两种内容可以分别控制显示和隐藏,也可以同时显示,本文介绍的等待框就是使用了同时显示。
三、等待动画用户控件
按照设想,等待框的动画部分作为自定义内容放入弹框的 contentcontrol 中,所以我们需要新建个用户控件。(此节参考朝夕教育 jovan 老师在 b 站发布的 wpf 教学视频的“动画实战”一节)
将一个 grid 分为四列,每列中放置一个不同颜色的 border (以 grid 包裹)并设置 layouttransform 变换类型为 scaletransform,并给每个 scaletransform 命名:
border 显示为圆形并居中的代码为:
<grid.resources>
<style targettype="border">
<setter property="width" value="{binding relativesource={relativesource ancestortype=grid}, path=actualwidth, converter={staticresource divideconverter}, converterparameter=2}"></setter>
<setter property="height" value="{binding relativesource={relativesource self}, path=width}"></setter>
<setter property="cornerradius" value="100"></setter>
<!--<setter property="layouttransform">
<setter.value>
<scaletransform scalex="1.6" scaley="1.6"></scaletransform>
</setter.value>
</setter>-->
</style>
</grid.resources>
也就是设置宽度为包裹它的 grid 的宽度的一半,即每列宽度的一半,这个平分的操作是通过转换器 divideconverter 实现的,具体可下载代码查看。然后,高度绑定宽度,这样就是正方形了。最后再设置圆角,就成圆形了。注释的部分是设置 layouttransform 变换的,具体的 scaletransform 变换有个 scalex 和 scaley 值,分别设置 x 和 y 方向上的变换数值(变大为 1.6 倍),由于后面需要对这两个值设置动画,所以此处不能写死,注释掉。
动画直接在后台设置:
private void uc_wait_onloaded(object sender, routedeventargs e)
{
runanimation();
}
private void runanimation()
{
//定义动画;
doubleanimation da = new doubleanimation()
{
duration = new duration(timespan.frommilliseconds(1000)),
to = 1.6,
repeatbehavior = repeatbehavior.forever,
autoreverse = true,
};
task.run(async () =>
{
for (int i = 0; i < 4; i++)
{
dispatcher.invoke(() =>
{
var st = findname($"st{i + 1}") as scaletransform;
st?.beginanimation(scaletransform.scalexproperty, da);
st?.beginanimation(scaletransform.scaleyproperty, da);
});
await task.delay(300);
}
});
}
界面载入后执行动画方法,动画方法中先定义了一个 doubleanimation 类型的动画:间隔一秒,目标值为 1.6,一直重复,自动反转。然后在循环中按照命名规则,依次先使用 findname 方法找到 scaletransform 元素对象,并对其设置 x 和 y 方向上的动画,等待 300 毫秒再设置下一个,总共四个。
四、弹窗 viewmodel 和帮助类的改造
弹窗 viewmodel 中添加了一个标识是否是等待框的属性 iswaitdialog,在倒计时计时器里面,当是等待框时改为正计时,自然也就不会触发关闭操作,代码如下:
/// <summary>
/// 是否是等待框
/// </summary>
public bool iswaitdialog { get; set; } = false;
/// <summary>
/// 倒计时计时器
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void timer_elapsed(object sender, elapsedeventargs e)
{
if (iswaitdialog)
{
lefttime++;
}
else
{
lefttime--;
if (lefttime <= 0)
{
_timer.stop();
closecommand.execute(null);
}
}
}
在控制弹框显示隐藏的属性 isshowdialog 的 set 方法中,当是等待框时,倒计时设为零,方便后面(上面说的)直接进行正计时:
关键是帮助方法中,新增一个弹出等待框方法:
/// <summary>
/// 弹出等待框
/// </summary>
/// <param name="vm">相关viewmodel</param>
/// <param name="message">消息内容</param>
/// <param name="action">业务方法</param>
/// <param name="title">弹窗标题</param>
/// <returns></returns>
public static async task showwait(confirmboxviewmodel vm, string message, func<task> action = null, string title = "请耐心等待")
{
vm.customcontent = new uc_wait();
await task.run(async () =>
{
vm.ismessagedialog = false;
vm.iswaitdialog = true;
vm.isshowdialog = true;
vm.isshowtext = true;
vm.isshowcustom = true;
vm.isshowbutton = false;
vm.customcontenthorizontalalignment = horizontalalignment.stretch.tostring();
if (!string.isnullorwhitespace(message))
{
vm.dialogmessage = message;
}
if (!string.isnullorwhitespace(title))
{
vm.dialogtitle = title;
}
console.writeline($"等待框就绪,业务操作开始执行...");
await task.run(async () =>
{
await action?.invoke();
}).continuewith(_ =>
{
vm.isshowdialog = false;
console.writeline($"业务操作执行完毕,等待框关闭.");
});
});
}
先将自定义内容设置为等待动画用户控件,接下来是一些显示方面的设置。
关键是如何在执行完业务方法后才关闭弹窗呢?
一开始 func<task> action 这个参数我用的还是 action action,这样的话,action?.invoke() 这里不能 await,然后 .net core 3.1 又不支持 action?.begininvoke(callback, null) 这种写法。
后来把参数类型改为 func<task> ,就可以 await action?.invoke() 了,而且神奇的是,调用的地方不用修改(后面展示)。这样的话,就可以通过如下方式(continuewith)达到业务方法执行完成之后关闭弹窗了:
console.writeline($"等待框就绪,业务操作开始执行...");
await task.run(async () =>
{
await action?.invoke();
}).continuewith(_ =>
{
vm.isshowdialog = false;
console.writeline($"业务操作执行完毕,等待框关闭.");
});
五、使用方法和代码地址
使用就比较简单了:
waitcommand ??= new relaycommand(o => true, async o =>
{
await confirmboxhelper.showwait(dialogvm, "正在执行业务操作...", async () =>
{
await task.delay(1000 * 10);
console.writeline("操作完成");
});
});
代码地址:https://gitee.com/dlgcy/wpftemplate