让用户尽量少打字
每次让用户输入这么多信息的确很糟糕, 可以改进一下设计: 服务器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); |