首页 > 代码库 > 对delegate进行扩展 打造通用的"计时完成"方法 z

对delegate进行扩展 打造通用的"计时完成"方法 z

让用户尽量少打字

        每次让用户输入这么多信息的确很糟糕, 可以改进一下设计: 服务器IP和用户名可以存放在配置文件里面, 初始化的时候默认加载到相应的文本框中; 从安全角度考虑, 密码必须经过用户手动输入; 而数据库名字则没必要让用户输入, 有了服务器IP、用户名、密码后可以尝试连接SQL Server, 连接SQL Server成功后, 把数据库中所有的数据库名加载到ComboBox让用户选择连接哪个数据库.

密码不正确

密码正确

如何实现

        在后台代码中定义一个计时器, 设置它的Interval为1000毫秒, 用户输入密码时让定时器重新计时, 也就是说用户输入密码后, 如果在1秒钟内用户没有继续输入密码, 则会触发计时器的Elapsed事件, 这时程序尝试能不能连上SQL Server.

1
2
3
4
5
6
7
8
9
10
11
//用户停止输入密码1秒后自动尝试连接
private System.Timers.Timer m_timer=new System.Timers.Timer(1000);
         
private void txtPassword_PasswordChanged(object sender, RoutedEventArgs e)
{
    m_timer.Stop();
    m_timer.Start();
 
    //清空数据库的选择列表
    cbbDatabase.Items.Clear();
}

        在计时器的Elapsed事件函数中, 程序尝试连接SQL Server, 但是如下写法会出现问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void timer_Elapsed(object sender, ElapsedEventArgs e)
{
    Dispatcher.BeginInvoke((Action)delegate()
    {
        var conStr = String.Format("Data Source={0};Integrated Security=False;User ID={1};Password={2};", txtServer.Text, txtUser.Text, txtPassword.Password);
        using (var con = new SqlConnection(conStr))
        {
            try
            {
                con.Open();
            }
            catch
            {
                //TODO: 提示连接数据库失败
                return;
            }
            //TODO:提示连接数据库成功
 
            var sql = "select name from sys.databases";
            var cmd = new SqlCommand(sql, con);
            IAsyncResult asyncResult = cmd.BeginExecuteReader();
            SqlDataReader reader = cmd.EndExecuteReader(asyncResult);
            if (!reader.HasRows)
            {
                //TODO: 提示SQL Server中不存在数据库
                return;
            }
            //TODO: 在这里释放Timer
 
            while (reader.Read())
            {
                cbbDatabase.Items.Add(reader[0].ToString());
            }
        }
    });
}

         如果数据库的IP是错误的, con.Open()则要花费约30秒左右才会抛出异常, 这段时间UI线程会一直卡死, 经过测试, 30秒后才抛异常与数据库连接字符串的Connect Timeout属性没有关系.

         可以写一个SqlConnection的扩展方法, 在这个方法里面用一个码表对执行con.Open()的时间计时, 用一个bool型变量标识有没有成功的con.Open(), 如果在规定的时间内没有成功连上SQL Server则抛出一个异常.

扩展SqlConnection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static class SqlExtension
{
    public static void TryOpen(this SqlConnection connection, int millisecondTimeout)
    {
        Stopwatch sw = new Stopwatch();
        bool succeed = false;
 
        Thread t = new Thread(() =>
            {
                try
                {
                    sw.Start();
                    connection.Open();
                    //打开连接后设置succeed为true
                    succeed = true;
                }
                catch
                {
                }
            });
        t.IsBackground = true;
        t.Start();
        t.Join(millisecondTimeout);
        t.Abort();
 
        //如果没有成功, 则抛出一个异常
        if (!succeed)
            throw new Exception();
    }
}

        这样在原来的代码中调用con.TryOpen()就可以解决问题.

扩展delegate

        其实扩展方法的第一个参数并不是一定要是一个class类, 其实delegate也是可以的, delegate其实在编译之后就是一个类, 如下改写刚刚的SqlConnection扩展方法, 就可以写一个通用的尝试操作的扩展方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static class ActionExtension
{
    public static void Invoke(this Action action, int millisecondTimeout)
    {
        Stopwatch sw = new Stopwatch();
        bool succeed = false;
 
        Thread t = new Thread(() =>
            {
                try
                {
                    sw.Start();
                    action();
                    succeed = true;
                }
                catch
                {
                }
            });
        t.IsBackground = true;
        t.Start();
        t.Join(millisecondTimeout);
 
        if (!succeed)
            throw new Exception();
    }
}

        这样写, 代码则更为通用一些, 以后一些耗时的操作(当然要满足Action的签名)都可以调用这个方法, 在本例中, 可以如下调用:

1
((Action)con.Open).Invoke(2000);