并发编程概述
前言
基础术语
同步(synchronization):关于协调线程或进程之间的活动,并确保被多个线程或进程访问的数据一直有效,同步允许线程和进程一致地操作。
进程(Process):一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配和调度运行的基本单位,当一个程序开始运行时,它在系统中奖开启一个或多个进程,一个进程又有多个线程组成。
线程(thread):代表程序中的单个执行逻辑流程,是一个独立处理的执行路径,是轻量级的进程。
多线程(multithreading):多个线程来执行程序。并发的一种形式,但不是唯一的方式。
并行处理:把正在执行的大量的任务分割成小块,分配给多个同时运行的线程。多线程的一种。
异步编程:并发的一种形式,采用future模式或回调(callback)机制,以避免产生不必要的线程。
异步编程
异步编程并不一定要用多线程去实现,多线程只是其中一种实现手段。在.Net中,新版funture类型有Task和Task<TResult>。老式异步编程API中采用回调或事件(event)。异步编程的核心理念是异步操作。
异步操作:启动了的操作将会在一段时间后完成。这个操作执行时,不会阻塞原来的线程。启动了这个操作的线程,可以继续执行其他任务。当操作完成时,会通知它的future或调用回调函数,以便让程序指导操作已经结束。
响应式编程:一种声明式的编程模式,程序在该模式中对事件做出响应,区别于异步编程是因为它是基于异步事件(asynchronous evnt)。并发编程的一种形式。
I/O密集与计算密集
I/O密集(I/O-bound):如果一个操作将大部分时间用于等待一个条件的产生,那么它就被成为I/O密集操作。
并发编程
委托(delegate)
委托概述
将方法调用者和目标方法动态关联起来,委托是一个类,所以它和类是同级的,可以通过委托来掉用方法,不要误以为委托和方法同级的,方法只是类的成员。委托定义了方法的类型(定义委托和与之对应的方法必须具有相同的参数个数,并且类型相同,返回值类型相同),使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。
基础委托(Delegate)
在.Net中声明委托使用关键词delegate,委托具有多种使用方式(以下均为同步委托调用):
1 /// <summary> 2 /// 普通委托基础调用方式(同步委托) 3 /// </summary> 4 public class Delegates 5 { 6 /// <summary> 7 /// 定义有参无返回值委托 8 /// </summary> 9 /// <param name="i"></param> 10 public delegate void NoReturnWithParameters(string o); 11 /// <summary> 12 /// 构造函数实例化 13 /// </summary> 14 public void DemoOne() 15 { 16 NoReturnWithParameters methord = new NoReturnWithParameters(this.Test); 17 methord.Invoke("One-ok"); 18 } 19 /// <summary> 20 /// 赋值对象 21 /// </summary> 22 public void DemoTwo() 23 { 24 NoReturnWithParameters methord = this.Test; 25 methord.Invoke("Two-ok"); 26 } 27 /// <summary> 28 /// DotNet 2.0 29 /// </summary> 30 public void DemoThree() 31 { 32 NoReturnWithParameters methord = new NoReturnWithParameters( 33 delegate (string o) 34 { 35 Console.WriteLine("有参无返回值:{0}", o); 36 } 37 ); 38 methord.Invoke("Three-ok"); 39 } 40 /// <summary> 41 /// DotNet 3.0 42 /// </summary> 43 public void DemoFour() 44 { 45 NoReturnWithParameters methord = new NoReturnWithParameters( 46 (string o) => 47 { 48 Console.WriteLine("有参无返回值:{0}", o); 49 } 50 ); 51 methord.Invoke("Four-ok"); 52 } 53 /// <summary> 54 /// 委托约束 55 /// </summary> 56 public void DemoFive() 57 { 58 NoReturnWithParameters methord = new NoReturnWithParameters( 59 (o) => 60 { 61 Console.WriteLine("有参无返回值:{0}", o); 62 } 63 ); 64 methord.Invoke("Five-ok"); 65 } 66 /// <summary> 67 /// 方法只有一行去则掉大括号及分号 68 /// </summary> 69 public void DemoSix() 70 { 71 NoReturnWithParameters methord = new NoReturnWithParameters((o) => Console.WriteLine("有参无返回值:{0}", o)); 72 methord.Invoke("Six-ok"); 73 } 74 public void DemoSeven() 75 { 76 NoReturnWithParameters methord = (o) => Console.WriteLine("有参无返回值:{0}", o); 77 methord.Invoke("Seven-ok"); 78 } 79 /// <summary> 80 /// 定义有参无返回值测试方法 81 /// </summary> 82 /// <param name="o"></param> 83 private void Test(string o) 84 { 85 Console.WriteLine("有参无返回值:{0}", o); 86 } 87 /* 88 * 作者:Jonins 89 * 出处:http://www.cnblogs.com/jonins/ 90 */ 91 }
同步委托&异步委托
同步委托:委托的Invoke方法用来进行同步调用。同步调用也可以叫阻塞调用,它将阻塞当前线程,然后执行调用,调用完毕后再继续向下进行。
异步委托:异步调用不阻塞线程,而是把调用塞到线程池中,程序主线程或UI线程可以继续执行。委托的异步调用通过BeginInvoke和EndInvoke来实现。
以下为异步委托调用方式:
1 class Program 2 { 3 /// <summary> 4 /// 定义有参无返回值委托 5 /// </summary> 6 /// <param name="i"></param> 7 public delegate void NoReturnWithParameters(string o); 8 static void Main(string[] args) 9 { 10 NoReturnWithParameters methord = new NoReturnWithParameters(Test); 11 Console.WriteLine("主线程执行1"); 12 Console.WriteLine("主线程执行2"); 13 methord.BeginInvoke("demo-ok", null, null); 14 Console.WriteLine("主线程执行3"); 15 Console.WriteLine("主线程执行4"); 16 Console.ReadKey(); 17 } 18 /// <summary> 19 /// 异步调用委托方法 20 /// </summary> 21 /// <param name="o"></param> 22 static void Test(string o) 23 { 24 Console.WriteLine("有参无返回值:{0}", o); 25 } 26 /* 27 * 作者:Jonins 28 * 出处:http://www.cnblogs.com/jonins/ 29 */ 30 }
因为调用BeginInvoke为异步委托,不会阻塞主线程,运行结果如下:
异步回调(Callback)
异步回调通过设置回调函数,当调用结束时会自动调用回调函数,可以在回调函数里触发EndInvoke,这样就释放掉了线程,可以避免程序一直占用一个线程。
1 class Program 2 { 3 /// <summary> 4 /// 定义有参有返回值委托 5 /// </summary> 6 /// <param name="i"></param> 7 public delegate string ReturnWithParameters(string o); 8 static void Main(string[] args) 9 { 10 ReturnWithParameters methord = new ReturnWithParameters(Test); 11 Console.WriteLine("主线程执行1"); 12 Console.WriteLine("主线程执行2"); 13 /* 14 BeginInvoke方法参数个数不确定, 最后两个参数含义固定,如果不使用的话,需要赋值null 15 委托的方法无参数,这种情况下BeginInvoke中只有两个参数。 16 此外,委托的方法有几个参数,BeginInvoke中从左开始,对应响应的参数。 17 1.倒数第二个参数:是有一个参数值无返回值的委托,它代表的含义为,该线程执行完毕后的回调。 18 2.倒数第一个参数:向即回调中传值,用AsyncState来接受。 19 3.其它参数:对应委托方法的参数。 20 */ 21 IAsyncResult asyncResult = methord.BeginInvoke("demo-ok", new AsyncCallback(Callback), "AsycState:给回调函数的参数传递在此处出传值"); 22 Console.WriteLine("主线程执行3"); 23 Console.WriteLine("主线程执行4"); 24 Console.ReadKey(); 25 } 26 /// <summary> 27 /// 异步调用委托方法 28 /// </summary> 29 /// <param name="o"></param> 30 /// <returns></returns> 31 private static string Test(string o) 32 { 33 return "委托方法执行成功:" + o; 34 } 35 /// <summary> 36 /// 回调函数 37 /// </summary> 38 /// <param name="asyncResult"></param> 39 private static void Callback(IAsyncResult asyncResult) 40 { 41 /* 42 *asyncResult为回调前异步调用方法返回值 43 *AsyncResult 是IAsyncResult接口的一个实现类,引用空间:System.Runtime.Remoting.Messaging 44 *AsyncDelegate 属性可以强制转换为定义的委托类型 45 */ 46 ReturnWithParameters methord = (ReturnWithParameters)((System.Runtime.Remoting.Messaging.AsyncResult)asyncResult).AsyncDelegate; 47 Console.WriteLine(methord.EndInvoke(asyncResult)); 48 Console.WriteLine(asyncResult.AsyncState); 49 } 50 /* 51 * 作者:Jonins 52 * 出处:http://www.cnblogs.com/jonins/ 53 */ 54 }
执行结果如下:
注意:
1.异步调用只能调用一次EndInvoke,否则会报错。
2.如果不回调函数中执行EndInvoke,请在异步调用后手动执行EndInvoke方法释放资源。
异步委托线程等待
1.【Delegate】.EndInvoke(推荐)
1 public delegate void NoReturnWithParameters(string o); 2 NoReturnWithParameters noReturnWithParameters = new NoReturnWithParameters(...); 3 ...... 4 noReturnWithParameters.EndInvoke(asyncResult);
2.【IAsyncResult】.AsyncWaitHandle.WaitOne(可以定义等待时间,超过等待时间不继续等待向下执行)
1 IAsyncResult asyncResult = null; 2 asyncResult.AsyncWaitHandle.WaitOne(2000);//等待2000毫秒,超时不等待
3.【IAsyncResult】.IsCompleted(是IAsyncResult对象的一个属性,该值指示异步操作是否已完成。不推荐)
1 IAsyncResult asyncResult = xxx.BeginInvoke(...); 2 while (!asyncResult.IsCompleted) 3 { 4 //正在等待中 5 }
内置委托(泛化委托)
.Net Framework 提供两个支持泛型的内置委托,分别是Action<>和Func<>,在System命名空间中定义,结合lambda表达式,可以提高开发效率。
使用方式如下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //使用Action声明委托 6 Action<string> action = TestAction; 7 action.Invoke("action-demo-ok"); 8 //使用Func声明委托 9 Func<string, string> func = TestFunc; 10 string result = func.Invoke("func-demo-ok"); 11 Console.WriteLine(result); 12 Console.ReadKey(); 13 } 14 private static void TestAction(string o) 15 { 16 Console.WriteLine("TestAction方法执行成功:{0}", o); 17 } 18 private static string TestFunc(string o) 19 { 20 return "TestFunc方法执行成功:" + o; 21 } 22 /* 23 * 作者:Jonins 24 * 出处:http://www.cnblogs.com/jonins/ 25 */ 26 }
Action:无返回值的泛型委托,目前.NET Framework提供了17个Action委托,它们从无参数到最多16个参数。
public delegate void Action | |
Action | 无返回值的泛型委托 |
Action<int,string> | 传入参数int、string,无返回值的委托 |
Action<int,string,bool> | 传入参数int,string,bool,无返回值的委托 |
Action<bool,bool,bool,bool> | 传入4个bool型参数,无返回值的委托 |
Action最少0个参数,最多16个参数,无返回值。 |
Func:有返回值的泛型委托,.NET Framework提供了17个Func函数,允许回调方法返回值。
public delegate TResult Func | |
Func<int> | 无参,返回值为int的委托 |
Func<int,string> | 传入参数int,返回值为string类型的委托 |
Func<object,string,bool> | 传入参数为object, string 返回值为bool类型的委托 |
Func<T1,T2,,T3,int> 表示 | 传入参数为T1,T2,,T3(类型)返回值为int类型的委托 |
Func最少0个参数,最多16个参数,根据返回值泛型返回。必须有返回值,不可为void。 |
本质上Action和Func都为delegate ,在System命名空间中定义(in和out用来标识变量)
除此之外还有Predicate,它是固定返回值为bool类型的泛型委托。Action和Func足够使用这里不做介绍。
注意:
1.委托定义不要太多,微软仅在MSCorLib.dll中就有进50个委托类型,而且.NET Framework现在支持泛型,所以我们只需几个泛型委托(在System命名空间中定义)就能表示需要获取多达16个参数的方法。
2.如需获取16个以上参数,就必须定义自己的委托类型。所以建议尽量使用内置委托,而不是在代码中定义更多的委托类型,这样可以减少代码中的类型数量,同时简化编码。
3.如需使用ref或out关键字以传引用的方式传递参数,就需要定义自己的委托。
内置委托(泛化委托)参数协变&逆变
协变(out):假定S是B的子类,如果X(S)允许引用转换成X(B),那么称X为协变类。(支持“子类”向“父类”转换)
逆变(in):假定S是B的子类,如果X(B)允许引用转换成X(X),那么称X为协变类。(支持“父类”向“子类”转换)
正如泛化接口,泛型委托同样支持协变与逆变
1 public delegate void Action<in T>(T obj); 2 3 public delegate TResult Func<out TResult>();
Action在System命名空间中定义支持逆变(in)
1 Action<object> x =...; 2 3 Action<string> y = x;
Func在System命名空间中定义支持协变(out)
1 Func<string> x =...; 2 3 Func<object> y = x;
如果要定义一个泛化委托类型,最好按照如下准则:
1.将只用在返回值的类型参数标注为协变(out)
2.将只用在参数的类型参数标注为逆变(in)
委托的兼容性
了解委托的兼容性,更易于在使用委托时使我们构建的代码具有多态性。
1.类型的兼容性:即使签名相似,委托类也互不兼容。
1 delegate void D1(); 2 delegate void D2(); 3 ... 4 D1 d1=Method1; 5 D2 d2=d1;//编译时错误 6 D2 d2=new D2(d1);//这是允许的
如果委托实例执行相同的目标方法,则认为它们是等价的。
1 delegate void D(); 2 ... 3 D1 d1=Method1; 4 D2 d2=Method1; 5 Console.WriteLine(d1==d2);//True
如果多播委托按照相同的顺序应用相同的方法责任委托它们是等价的。
2.参数的兼容性:当调用一个方法时,可以给方法的参数提供大于其指定类型的变量。这是正常的多态行为。同样,委托也可以又大于其目标方法参数类型的参数,即逆变。
1 class Program 2 { 3 //委托接受string类型参数 4 delegate void NoReturnWithParameters(string o); 5 static void Main(string[] args) 6 { 7 NoReturnWithParameters noReturnWithParameters = new NoReturnWithParameters(Test); 8 noReturnWithParameters("demo-ok"); 9 Console.ReadKey(); 10 } 11 //目标方法接受object类型参数 12 static void Test(object o) 13 { 14 Console.WriteLine("返回值:{0}", o); 15 } 16 }
上述代码将参数string在调用目标方法时隐式向上转换为Object。
3.返回类型的兼容性:如果调用一个方法,得到的返回值类型可能大于请求的类型,这是正常多态行为。同样,委托的返回类型可以小于它的目标方法的返回值类型即协变。
1 class Program 2 { 3 //委托返回object类型 4 delegate object NoReturnWithParameters(string o); 5 static void Main(string[] args) 6 { 7 NoReturnWithParameters noReturnWithParameters = new NoReturnWithParameters(Test); 8 object o = noReturnWithParameters("demo-ok"); 9 Console.WriteLine(o); 10 Console.ReadKey(); 11 } 12 //目标方法返回string类型 13 static string Test(string o) 14 { 15 return "返回值:" + o; 16 } 17 }
注意:标准事件模式的设计宗旨时再其使用公共基类EventArgs时应用逆变。例如,可以用两个不同的委托调用同一个方法,一个传递MouseEventArgs,另一个传递KeyEventArgs。
多播委托(+=&-=)
1 class Program 2 { 3 public delegate int MulticastInstance(int inputA, int inputB); 4 static void Main(string[] args) 5 { 6 MulticastInstance multicastInstance = Addition; 7 multicastInstance += new MulticastInstance(Reduce); 8 multicastInstance += new MulticastInstance(Multiply); 9 int result = multicastInstance(10, 5); 10 Console.WriteLine("最后执行得到的结果为:{0}", result); 11 Console.ReadKey(); 12 } 13 /// <summary> 14 /// 加法 15 /// </summary> 16 /// <param name="inputA"></param> 17 /// <param name="inputB"></param> 18 /// <returns></returns> 19 private static int Addition(int inputA, int inputB) 20 { 21 int result = inputA + inputB; 22 Console.WriteLine("Addition方法执行结果:{0}", result); 23 return result; 24 } 25 /// <summary> 26 /// 减法 27 /// </summary> 28 /// <param name="inputA"></param> 29 /// <param name="inputB"></param> 30 /// <returns></returns> 31 private static int Reduce(int inputA, int inputB) 32 { 33 int result = inputA - inputB; 34 Console.WriteLine("Reduce方法执行结果:{0}", result); 35 return result; 36 } 37 /// <summary> 38 /// 乘法 39 /// </summary> 40 /// <param name="inputA"></param> 41 /// <param name="inputB"></param> 42 /// <returns></returns> 43 private static int Multiply(int inputA, int inputB) 44 { 45 int result = inputA * inputB; 46 Console.WriteLine("Multiply方法执行结果:{0}", result); 47 return result; 48 } 49 /* 50 * 作者:Jonins 51 * 出处:http://www.cnblogs.com/jonins/ 52 */ 53 }
得到的结果如下:
委托模拟观察者
能用委托解决的问题,都可以用接口解决。但再下面的情形中,委托可能是比接口更好的选择:
1.接口内之定义一个方法
2.需要多播能力
3.订阅者需要多次实现接口
下面代码是委托的观察者模式,优点是解耦且符合开放封闭原则:
1 public class MulticastDelegates 2 { 3 public delegate int MulticastInstance(int inputA, int inputB); 4 /// <summary> 5 /// 模拟观察者 6 /// </summary> 7 public void Demo() 8 { 9 Manager manager = new Manager(); 10 manager.Attach(new MulticastInstance(Add)); 11 manager.Attach(new MulticastInstance(Reduce)); 12 manager.Attach(new MulticastInstance(Multiply)); 13 manager.Execute(10, 5); 14 } 15 /// <summary> 16 /// Observer模式、又称呼发布订阅或监听模式 17 /// </summary> 18 public class Manager 19 { 20 private MulticastInstance Handler; 21 22 /// <summary> 23 /// 附加观察者 24 /// </summary> 25 /// <param name="handler1"></param> 26 public void Attach(MulticastInstance handler1) 27 { 28 Handler += handler1; 29 } 30 /// <summary> 31 /// 分离观察者 32 /// </summary> 33 /// <param name="handler1"></param> 34 public void Detach(MulticastInstance handler1) 35 { 36 Handler -= handler1; 37 } 38 /// <summary> 39 /// 如果观察者数量大于0即执行播委托列表中的方法 40 /// </summary> 41 /// <param name="inputA"></param> 42 /// <param name="inputB"></param> 43 public void Execute(int inputA, int inputB) 44 { 45 if (Handler != null) 46 if (Handler.GetInvocationList().Count() != 0) 47 Handler(inputA, inputB); 48 } 49 } 50 private int Add(int inputA, int inputB) 51 { 52 int result = inputA + inputB; 53 Console.WriteLine("Add方法执行结果:{0}", result); 54 return result; 55 } 56 private int Reduce(int inputA, int inputB) 57 { 58 int result = inputA - inputB; 59 Console.WriteLine("Reduce方法执行结果:{0}", result); 60 return result; 61 } 62 private int Multiply(int inputA, int inputB) 63 { 64 int result = inputA * inputB; 65 Console.WriteLine("Multiply方法执行结果:{0}", result); 66 return result; 67 } 68 }
委托揭秘
1 public delegate int MulticastInstance(int inputA, int inputB);
事实上通过反编译可看到:
编译器相当于定义了一个完整的类(继承自System.MulticastDelegate,定义四个方法:构造函数、Invoke、BeginInvoke和EndInvoke):
1 internal class MulticastInstance : System.MulticastDelegate//继承System.MulticastDelegate 2 { 3 //构造器 4 public MulticastInstance(object @object, IntPtr method); 5 //这个方法的原型和源代码指定的一样 6 public virtual int Invoke(int inputA, int inputB); 7 //实现回调方法和异步回调 8 public virtual IAsyncResult BeginInvoke(int inputA, int inputB, AsyncCallback callback, object @object); 9 public virtual int EndInvoke(IAsyncResult result); 10 } 11 /* 12 * 作者:Jonins 13 * 出处:http://www.cnblogs.com/jonins/ 14 */
所有委托类型都派生自System.MulticastDelegate类,System.MulticastDelegate派生自System.Delegate,后者又派生自System.Object。历史原因造成有两个委托类。
创建的所有委托类型豆浆MulticastDelegate作为基类,个别情况下仍会用到Delegate。Delegate类的两个静态方法Combine和Remove的签名都指出要获取Delegate参数。由于创建的委托类型派生自MulticastDelegate,后者又派生自Delegate,所以委托类型的实例是可以传递给这两个方法的。
MulticastDelegate的三个重要非公共字段
字段 | 类型 | 说明 |
_target | System.Object |
当委托对象包装一个静态方法时,这个字段为null。当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象。 当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象。换言之 换言之,这个字段指出要传给实例方法的隐士参数的值。 |
_methodPtr | System.IntPtr |
一个内部的整数值,CLR用它标记要回调的方法。 |
_invocationList | System.Object | 该字段通常为null,构造委托链时它引用一个委托数组。 |
Delegate反编译后可看到静态方法Combine和Remove(委托的+、-、+=、-=编译后的本质):
1 [Serializable, ClassInterface(ClassInterfaceType.AutoDual), ComVisible(true), __DynamicallyInvokable] 2 public abstract class Delegate : ICloneable, ISerializable 3 { 4 [ComVisible(true), __DynamicallyInvokable] 5 public static Delegate Combine(params Delegate[] delegates); 6 [__DynamicallyInvokable] 7 public static Delegate Combine(Delegate a, Delegate b); 8 [SecuritySafeCritical, __DynamicallyInvokable] 9 public static Delegate Remove(Delegate source, Delegate value); 10 }
结语
同步委托将阻塞当前线程,等待方法执行完毕继续执行程序,相当于直接调用方法。异步委托是将方法放入线程池中执行并不阻塞主线程。异步委托从根本上说并不是多线程技术(任务Task也一样),就算异步委托内部将方法塞给线程池去执行也并不能说是开辟新线程执行方法,(线程池一定开辟新线程)这种说法并不严谨。委托本质是将调用者和目标方法动态关联起来,这是或许是我所理解的委托存在的最根本目的吧。
参考文献
CLR via C#(第4版) Jeffrey Richter
C#高级编程(第7版) Christian Nagel
果壳中的C# C#5.0权威指南 Joseph Albahari
事件(event)
事件概述
委托是一种类型可以被实例化,而事件可以看作将多播委托进行封装的一个对象成员(简化委托调用列表增加和删除方法)但并非特殊的委托,保护订阅互不影响。
基础事件(event)
在.Net中声明事件使用关键词event,使用也非常简单在委托(delegate)前面加上event:
1 class Program 2 { 3 /// <summary> 4 /// 定义有参无返回值委托 5 /// </summary> 6 /// <param name="i"></param> 7 public delegate void NoReturnWithParameters(); 8 /// <summary> 9 /// 定义接受NoReturnWithParameters委托类型的事件 10 /// </summary> 11 static event NoReturnWithParameters NoReturnWithParametersEvent; 12 static void Main(string[] args) 13 { 14 //委托方法1 15 { 16 Action action = new Action(() => 17 { 18 Console.WriteLine("测试委托方法1成功"); 19 }); 20 NoReturnWithParameters noReturnWithParameters = new NoReturnWithParameters(action); 21 //事件订阅委托 22 NoReturnWithParametersEvent += noReturnWithParameters; 23 //事件取阅委托 24 NoReturnWithParametersEvent -= noReturnWithParameters; 25 } 26 //委托方法2 27 { 28 //事件订阅委托 29 NoReturnWithParametersEvent += new NoReturnWithParameters(() => 30 { 31 Console.WriteLine("测试委托方法2成功"); 32 }); 33 } 34 //委托方法3 35 { 36 //事件订阅委托 37 NoReturnWithParametersEvent += new NoReturnWithParameters(() => Console.WriteLine("测试委托方法3成功")); 38 } 39 //执行事件 40 NoReturnWithParametersEvent(); 41 Console.ReadKey(); 42 } 43 /* 44 * 作者:Jonins 45 * 出处:http://www.cnblogs.com/jonins/ 46 */ 47 }
上述代码执行结果:
事件发布&订阅
事件基于委托,为委托提供了一种发布/订阅机制。当使用事件时一般会出现两种角色:发行者和订阅者。
发行者(Publisher)也称为发送者(sender):是包含委托字段的类,它决定何时调用委托广播。
订阅者(Subscriber)也称为接受者(recevier):是方法目标的接收者,通过在发行者的委托上调用+=和-=,决定何时开始和结束监听。一个订阅者不知道也不干涉其它的订阅者。
来电->打开手机->接电话,这样一个需求,模拟订阅发布机制:
1 /// <summary> 2 /// 发行者 3 /// </summary> 4 public class Publisher 5 { 6 /// <summary> 7 /// 委托 8 /// </summary> 9 public delegate void Publication(); 10 11 /// <summary> 12 /// 事件 这里约束委托类型可以为内置委托Action 13 /// </summary> 14 public event Publication AfterPublication; 15 /// <summary> 16 /// 来电事件 17 /// </summary> 18 public void Call() 19 { 20 Console.WriteLine("显示来电"); 21 if (AfterPublication != null)//如果调用列表不为空,触发事件 22 { 23 AfterPublication(); 24 } 25 } 26 } 27 /// <summary> 28 /// 订阅者 29 /// </summary> 30 public class Subscriber 31 { 32 /// <summary> 33 /// 订阅者事件处理方法 34 /// </summary> 35 public void Connect() 36 { 37 Console.WriteLine("通话接通"); 38 } 39 /// <summary> 40 /// 订阅者事件处理方法 41 /// </summary> 42 public void Unlock() 43 { 44 Console.WriteLine("电话解锁"); 45 } 46 } 47 /* 48 * 作者:Jonins 49 * 出处:http://www.cnblogs.com/jonins/ 50 */
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //定义发行者 6 Publisher publisher = new Publisher(); 7 //定义订阅者 8 Subscriber subscriber = new Subscriber(); 9 //发行者订阅 当来电需要电话解锁 10 publisher.AfterPublication += new Publisher.Publication(subscriber.Unlock); 11 //发行者订阅 当来电则接通电话 12 publisher.AfterPublication += new Publisher.Publication(subscriber.Connect); 13 //来电话了 14 publisher.Call(); 15 Console.ReadKey(); 16 } 17 }
执行结果:
注意:
1.事件只可以从声明它们的类中调用, 派生类无法直接调用基类中声明的事件。
1 publisher.AfterPublication();//这行代码在Publisher类外部调用则编译不通过
2.对于事件在声明类外部只能+=,-=不能直接调用,而委托在外部不仅可以使用+=,-=等运算符还可以直接调用。
下面调用方式与上面执行结果一样,利用了委托多播的特性。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Publisher publisher = new Publisher(); 6 Subscriber subscriber = new Subscriber(); 7 //------利用多播委托------- 8 var publication = new Publisher.Publication(subscriber.Unlock); 9 publication += new Publisher.Publication(subscriber.Connect); 10 publisher.AfterPublication += publication; 11 //---------End----------- 12 publisher.Call(); 13 Console.ReadKey(); 14 } 15 }
自定义事件(EventArgs&EventHandler&事件监听器)
有过Windwos Form开发经验对下面的代码会熟悉:
1 private void Form1_Load(object sender, EventArgs e) 2 { 3 ... 4 }
在设计器Form1.Designer.cs中有事件的附加。这种方式属于Visual Studio IDE事件订阅。
1 this.Load += new System.EventHandler(this.Form1_Load);
在 .NET Framework 类库中,事件基于 EventHandler 委托和 EventArgs 基类。
基于EventHandler模式的事件:
1 /// <summary> 2 /// 事件监听器 3 /// </summary> 4 public class Consumer 5 { 6 private string _name; 7 8 public Consumer(string name) 9 { 10 _name = name; 11 } 12 public void Monitor(object sender, CustomEventArgs e) 13 { 14 Console.WriteLine($"Name:{_name}; 信息:{e.Message};到底要不要接呢?"); 15 } 16 } 17 /// <summary> 18 /// 定义保存自定义事件信息的对象 19 /// </summary> 20 public class CustomEventArgs : EventArgs//作为事件的参数,必须派生自EventArgs基类 21 { 22 public CustomEventArgs(string message) 23 { 24 this.Message = message; 25 } 26 public string Message { get; set; } 27 } 28 /// <summary> 29 /// 发布者 30 /// </summary> 31 public class Publisher 32 { 33 public event EventHandler<CustomEventArgs> Publication;//定义事件 34 public void Call(string w) 35 { 36 Console.WriteLine("显示来电." + w); 37 OnRaiseCustomEvent(new CustomEventArgs(w)); 38 } 39 //在一个受保护的虚拟方法中包装事件调用。 40 //允许派生类覆盖事件调用行为 41 protected virtual void OnRaiseCustomEvent(CustomEventArgs e) 42 { 43 //在空校验之后和事件引发之前。制作临时副本,以避免可能发生的事件。 44 EventHandler<CustomEventArgs> publication = Publication; 45 //如果没有订阅者,事件将是空的。 46 if (publication != null) 47 { 48 publication(this, e); 49 } 50 } 51 } 52 /// <summary> 53 /// 订阅者 54 /// </summary> 55 public class Subscriber 56 { 57 private string Name; 58 public Subscriber(string name, Publisher pub) 59 { 60 Name = name; 61 //使用c# 2.0语法订阅事件 62 pub.Publication += UnlockEvent; 63 pub.Publication += ConnectEvent; 64 } 65 //定义当事件被提起时该采取什么行动。 66 void ConnectEvent(object sender, CustomEventArgs e) 67 { 68 Console.WriteLine("通话接通.{0}.{1}", e.Message, Name); 69 } 70 void UnlockEvent(object sender, CustomEventArgs e) 71 { 72 Console.WriteLine("电话解锁.{0}.{1}", e.Message, Name); 73 } 74 } 75 /* 76 * 作者:Jonins 77 * 出处:http://www.cnblogs.com/jonins/ 78 */
调用方式:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Publisher pub = new Publisher(); 6 //加入一个事件监听 7 Consumer jack = new Consumer("Jack"); 8 pub.Publication += jack.Monitor; 9 Subscriber user1 = new Subscriber("中国移动", pub); 10 pub.Call("号码10086"); 11 Console.WriteLine("--------------------------------------------------"); 12 Publisher pub2 = new Publisher(); 13 Subscriber user2 = new Subscriber("中国联通", pub2); 14 pub2.Call("号码10010"); 15 Console.ReadKey(); 16 } 17 }
结果如下:
1.EventHandler<T>在.NET Framework 2.0中引入,定义了一个处理程序,它返回void,接受两个参数。
1 public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
第一个参数(sender)是一个对象,包含事件的发送者。
第二个参数(e)提供了事件的相关信息,参数随不同的事件类型而改变(继承EventArgs)。
.NET1.0为所有不同数据类型的事件定义了几百个委托,有了泛型委托EventHandler<T>后,不再需要委托了。
2.EventArgs,标识表示包含事件数据的类的基类,并提供用于不包含事件数据的事件的值。
1 [System.Runtime.InteropServices.ComVisible(true)] 2 public class EventArgs
3.同时可以根据编程方式订阅事件:
1 Publisher pub = new Publisher(); 2 pub.Publication += Close; 3 ... 4 //添加一个方法 5 static void Close(object sender, CustomEventArgs a) 6 { 7 // 关闭电话 8 }
4.Consumer类为事件监听器当触发事件时可获取当前发布者对应自定义信息对象,可以根据需要做逻辑编码,再执行事件所订阅的相关处理。增加事件订阅/发布机制的健壮性。
5.以线程安全的方式触发事件
1 EventHandler<CustomEventArgs> publication = Publication;
触发事件是只包含一行代码的程序。这是C#6.0的功能。在之前版本,触发事件之前要做为空判断。同时在进行null检测和触发之间,可能另一个线程把事件设置为null。所以需要一个局部变量。在C#6.0中,所有触发都可以使用null传播运算符和一个代码行取代。
1 Publication?.Invoke(this, e);
注意:尽管定义的类中的事件可基于任何有效委托类型,甚至是返回值的委托,但一般还是建议使用 EventHandler使事件基于 .NET Framework 模式。
线程安全方式触发事件
在上面的例子中,过去常见的触发事件有三种方式:
1 //版本1 2 if (Publication != null) 3 { 4 Publication();//触发事件 5 } 6 7 //版本2 8 var temp = Publication; 9 if (temp != null) 10 { 11 temp();//触发事件 12 } 13 14 //版本3 15 var temp = Volatile.Read(ref Publication); 16 if (temp != null) 17 { 18 temp();//触发事件 19 }
版本1会发生NullReferenceException异常。
版本2的解决思路是,将引用赋值到临时变量temp中,后者引用赋值发生时的委托链。所以temp复制后即使另一个线程更改了AfterPublication对象也没有关系。委托是不可变得,所以理论上行得通。但是编译器可能通过完全移除变量temp的方式对上述代码进行优化所以仍可能抛出NullReferenceException.
版本3Volatile.Read()的调用,强迫Publication在这个调用发生时读取,引用真的必须赋值到temp中,编译器优化代码。然后temp只有再部位null时才被调用。
版本3最完美技术正确,版本2也是可以使用的,因为JIT编译机制上知道不该优化掉变量temp,所以在局部变量中缓存一个引用,可确保堆应用只被访问一次。但将来是否改变不好说,所以建议采用版本3。
事件揭秘
我们重新审视基础事件里的一段代码:
1 public delegate void NoReturnWithParameters(); 2 static event NoReturnWithParameters NoReturnWithParametersEvent;
通过反编译我们可以看到:
编译器相当于做了一次如下封装:
1 NoReturnWithParameters parameters; 2 private event NoReturnWithParameters NoReturnWithParametersEvent 3 { 4 add { NoReturnWithParametersEvent+=parameters; } 5 remove { NoReturnWithParametersEvent-=parameters; } 6 } 7 /* 8 * 作者:Jonins 9 * 出处:http://www.cnblogs.com/jonins/ 10 */
声明了一个私有的委托变量,开放两个方法add和remove作为事件访问器用于(+=、-=),NoReturnWithParametersEvent被编译为Private从而实现封装外部无法触发事件。
1.委托类型字段是对委托列表头部的引用,事件发生时会通知这个列表中的委托。字段初始化为null,表明无侦听者等级对该事件的关注。
2.即使原始代码将事件定义为Public,委托字段也始终是Private.目的是防止外部的代码不正确的操作它。
3.方法add_xxx和remove_xxxC#编译器还自动为方法生成代码调用(System.Delegate的静态方法Combine和Remove)。
4.试图删除从未添加过的方法,Delegate的Remove方法内部不做任何事经,不会抛出异常或任何警告,事件的方法集体保持不变。
5.add和remove方法以线程安全的一种模式更新值(Interlocked Anything模式)。
结语
类或对象可以通过事件向其他类或对象通知发生的相关事情。事件使用的是发布/订阅机制,声明事件的类为发布类,而对这个事件进行处理的类则为订阅类。而订阅类如何知道这个事件发生并处理,这时候需要用到委托。事件的使用离不开委托。但是事件并不是委托的一种(事件是特殊的委托的说法并不正确),委托属于类型(type)它指的是集合(类,接口,结构,枚举,委托),事件是定义在类里的一个成员。
参考文献
CLR via C#(第4版) Jeffrey Richter
C#高级编程(第7版) Christian Nagel (版9、10对事件部分没有多大差异)
果壳中的C# C#5.0权威指南 Joseph Albahari
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/events/index
.net core 2.0 event bus 一个简单的基于内存事件总线实现
1.先定义一个事件接口
public interface IEvent { }
2.定义一个事件处理接口
public interface IEventHandler : IEvent { Task Handle(IEvent e); }
3.定义一个发布接口
public interface IEventPublisher { Task Publish<TEvent>(TEvent e) where TEvent : IEvent; }
4.定义一个订阅接口
public interface IEventSubscriber { Task Subscribe<TEvent, EH>() where TEvent : IEvent where EH : class, IEventHandler, new(); }
5.创建一个类用来存事件
public static class MemoryMq { public static ConcurrentDictionary<string, IEvent> eventQueueDict { get; set; } }
6.实现发布类
public class InMemoryEventPublisher : IEventPublisher { public Task Publish<TEvent>(TEvent @event) where TEvent : IEvent { if (@event == null) return Task.CompletedTask; if (MemoryMq.eventQueueDict == null) { MemoryMq.eventQueueDict = new ConcurrentDictionary<string, IEvent>(); } MemoryMq.eventQueueDict.GetOrAdd(Guid.NewGuid().ToString(),@event); return Task.CompletedTask; } }
7.实现订阅类
public class InMemoryEventSubscriber: IEventSubscriber { private readonly ConcurrentDictionary<string, Task> taskDict = new ConcurrentDictionary<string, Task>(); public Task Subscribe<TEvent, EH>() where TEvent : IEvent where EH : class, IEventHandler, new() { EH state = new EH(); Task.Run(() => { while (true) { if (MemoryMq.eventQueueDict != null) { foreach (var a in MemoryMq.eventQueueDict) { state.Handle(a.Value as IEvent); IEvent o; MemoryMq.eventQueueDict.TryRemove(a.Key ,out o); } } } }); return Task.CompletedTask; } }
9.测试用例
namespace MemoryMqTest { public class EventHandler : IEventHandler { public Task Handle(IEvent e, MessagingHelper h) { switch (e) { case Order value: Console.WriteLine(value.name); break; } return Task.CompletedTask; } } public class Order : IEvent { public string name { get; set; } } class Program { static void Main(string[] args) { var servicecollection = new ServiceCollection(); servicecollection.AddSingleton<IEventPublisher, InMemoryEventPublisher>(); servicecollection.AddSingleton<IEventSubscriber, InMemoryEventSubscriber>(); var provider = servicecollection.BuildServiceProvider(); var eventPub = provider.GetService<IEventPublisher>(); var _eventSub = provider.GetService<IEventSubscriber>(); _eventSub.Subscribe<Order, EventHandler>(); var order = new Order(); order.name = "test"; eventPub.Publish(order); Console.WriteLine("Hello World!"); Console.ReadKey(); } } }
10.测试结果
.net core 基于NPOI 的excel导出类,支持自定义导出哪些字段
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
/// <summary> /// 导出Excel
/// </summary>
/// <param name="lists"></param>
/// <param name="head">英文中文列名对照</param>
/// <param name="workbookFile">保存路径</param>
public static void getExcel<T>(List<T> lists, Dictionary< string , string > head, string workbookFile)
{
try
{
XSSFWorkbook workbook = new XSSFWorkbook();
using (MemoryStream ms = new MemoryStream())
{
var sheet = workbook.CreateSheet();
var headerRow = sheet.CreateRow(0);
bool h = false ;
int j = 1;
Type type = typeof (T);
PropertyInfo[] properties = type.GetProperties();
foreach (T item in lists)
{
var dataRow = sheet.CreateRow(j);
int i = 0;
foreach (PropertyInfo column in properties)
{
if (!h)
{
if (head.Keys.Contains(column.Name))
{
headerRow.CreateCell(i).SetCellValue(head[column.Name] == null ? column.Name : head[column.Name].ToString());
dataRow.CreateCell(i).SetCellValue(column.GetValue(item, null ) == null ? "" : column.GetValue(item, null ).ToString());
}
else
{
i -= 1;
}
}
else
{
if (head.Keys.Contains(column.Name))
{
dataRow.CreateCell(i).SetCellValue(column.GetValue(item, null ) == null ? "" : column.GetValue(item, null ).ToString());
}
else
{
i -= 1;
}
}
i++;
}
h = true ;
j++;
}
workbook.Write(ms);
using (FileStream fs = new FileStream(workbookFile, FileMode.Create, FileAccess.Write))
{
byte [] data = ms.ToArray();
fs.Write(data, 0, data.Length);
fs.Flush();
}
sheet = null ;
headerRow = null ;
workbook = null ;
}
}
catch (Exception ee)
{
string see = ee.Message;
}
}
|
基于Ace Admin 的菜单栏实现
1.首先是数据库表必然包含以下几个字段Id ,ParnetId,Url,Name等
create table dbo.Module ( Id uniqueidentifier not null constraint DF_Module_Id default newid(), Name varchar(255) collate Chinese_PRC_CI_AS not null constraint DF__Module__Name__46F27704 default ' ', Url varchar(255) collate Chinese_PRC_CI_AS not null constraint DF__Module__Url__47E69B3D default ' ', IsLeaf bit not null constraint DF__Module__IsLeaf__4AC307E8 default (1), IsAutoExpand bit not null constraint DF__Module__IsAutoEx__4BB72C21 default (0), IconName varchar(255) collate Chinese_PRC_CI_AS not null constraint DF__Module__IconName__4CAB505A default ' ', Status int not null constraint DF__Module__Status__4D9F7493 default (1), ParentName varchar(255) collate Chinese_PRC_CI_AS not null constraint DF__Module__ParentNa__4E9398CC default ' ', SortNo int not null constraint DF__Module__SortNo__507BE13E default (0), ParentId uniqueidentifier null )
2.服务端很简单,只要输出json格式就可以了
[
{
"Id": "bedb41a2-f310-4575-af99-01be01adda93",
"Name": "test",
"Url": "/",
"ParentId": "bedb41a2-f310-4775-af99-01be08adda93",
"IconName": "fa-users",
"Checked": false
},
{
"Id": "bedb41a2-f310-4775-af99-01be08adda93",
"Name": "角色管理",
"Url": "RoleManager.html",
"ParentId": "7580672f-a390-4bb6-982d-9a4570cb5199",
"IconName": "fa-users",
"Checked": false
},
{
"Id": "0031262c-689c-4b96-bae2-2c9d67076ade",
"Name": "流程设计",
"Url": "/flowmanage/flowdesign/index",
"ParentId": "7580672f-a390-4bb6-982d-9a4570cb5199",
"IconName": "fa-anchor",
"Checked": false
},
{
"Id": "e8dc5db6-4fc4-4795-a1cc-681cbcceec91",
"Name": "资源管理",
"Url": "/ResourceManager/Index",
"ParentId": "7580672f-a390-4bb6-982d-9a4570cb5199",
"IconName": "fa-calculator",
"Checked": false
},
{
"Id": "ef386d5d-cd58-43c0-a4ab-80afd0dbcd6c",
"Name": "用户管理",
"Url": "UserManager.html",
"ParentId": "7580672f-a390-4bb6-982d-9a4570cb5199",
"IconName": "fa-user",
"Checked": false
},
{
"Id": "7580672f-a390-4bb6-982d-9a4570cb5199",
"Name": "基础配置",
"Url": " ",
"ParentId": null,
"IconName": "fa-cog",
"Checked": false
},
{
"Id": "92b00259-2d15-43e7-9321-adffb29e8bf2",
"Name": "表单设计",
"Url": "/flowmanage/formdesign/index",
"ParentId": "7580672f-a390-4bb6-982d-9a4570cb5199",
"IconName": "fa-anchor",
"Checked": false
},
{
"Id": "bc80478d-0547-4437-9cff-be4b40144bdf",
"Name": "模块管理",
"Url": "ModuleManager.html",
"ParentId": "7580672f-a390-4bb6-982d-9a4570cb5199",
"IconName": "fa-file-code-o",
"Checked": false
},
{
"Id": "069475e3-c997-487a-9f29-e6a864c5c1d4",
"Name": "应用功能",
"Url": "/",
"ParentId": null,
"IconName": "fa-bars",
"Checked": false
},
{
"Id": "a94d5648-c2a9-405e-ba6f-f1602ec9b807",
"Name": "分类管理",
"Url": "/CategoryManager/Index",
"ParentId": "7580672f-a390-4bb6-982d-9a4570cb5199",
"IconName": "fa-archive",
"Checked": false
},
{
"Id": "6a9e1346-0c01-44d2-8eb1-f929fdab542a",
"Name": "部门管理",
"Url": "/OrgManager/Index",
"ParentId": "7580672f-a390-4bb6-982d-9a4570cb5199",
"IconName": "fa-plus-square-o",
"Checked": false
},
{
"Id": "89c3bfbe-246f-4112-8eb1-b6789da54202",
"Name": "进出库管理",
"Url": "/StockManager/Index",
"ParentId": "069475e3-c997-487a-9f29-e6a864c5c1d4",
"IconName": "fa-archive",
"Checked": false
},
{
"Id": "9486ff22-b696-4d7f-8093-8a3e53c45453",
"Name": "流程处理",
"Url": "/FlowManage/FlowInstances/Index",
"ParentId": "069475e3-c997-487a-9f29-e6a864c5c1d4",
"IconName": "fa-clock-o",
"Checked": false
}
]
3.重点在前端实现
(1)前端实现list转tree
/** * * */ var LTT, list, ltt; function pluck(collection, key) { return collection.map(function (item) { return item[key]; }); } function unique(collection) { return collection.filter(function (value, index, array) { return array.indexOf(value) === index; }); } function sortBy(collection, propertyA, propertyB) { return collection.sort(function (a, b) { if (a[propertyB] < b[propertyB]) { if (a[propertyA] > b[propertyA]) { return 1; } return -1; } else { if (a[propertyA] < b[propertyA]) { return -1; } return 1; } }); }; LTT = (function () { LTT.prototype.groupParent = []; LTT.prototype.key_id = 'id'; LTT.prototype.key_parent = 'parent'; LTT.prototype.key_child = 'child'; LTT.prototype.options = {}; function LTT(list, options) { this.list = list; this.options = options != null ? options : {}; this.ParseOptions(); //js不排序 //this.list = sortBy(this.list, this.key_parent, this.key_id); this.groupParent = unique(pluck(this.list, this.key_parent)); return this; } LTT.prototype.ParseOptions = function () { var that = this; ['key_id', 'key_parent', 'key_child'].forEach(function (item) { if (typeof that.options[item] !== 'undefined') { that[item] = that.options[item]; } }); }; LTT.prototype.GetParentItems = function (parent) { var item, result, _i, _len, _ref; result = []; _ref = this.list; for (_i = 0, _len = _ref.length; _i < _len; _i++) { item = _ref[_i]; if (item[this.key_parent] === parent) { result.push(item); } } return result; }; LTT.prototype.GetItemById = function (id) { var item, _i, _len, _ref; _ref = this.list; for (_i = 0, _len = _ref.length; _i < _len; _i++) { item = _ref[_i]; if (item[this.key_id] === id) { return item; } } return false; }; LTT.prototype.GetTree = function () { var child, i, obj, parentId, result, _i, _j, _len, _len1, _ref; result = []; _ref = this.groupParent; for (_i = 0, _len = _ref.length; _i < _len; _i++) { parentId = _ref[_i]; obj = this.GetItemById(parentId); child = this.GetParentItems(parentId); if (obj === false) { for (_j = 0, _len1 = child.length; _j < _len1; _j++) { i = child[_j]; result.push(i); } } else { obj[this.key_child] = child; } } return result; }; return LTT; })();
使用方法
// var ltt = new LTT(data, { key_id: 'Id', key_parent: 'ParentId', key_child:'Children' }); var tree = ltt.GetTree();
(2)菜单html拼接实现
//实现菜单 function getDom(data) { if(!data){return ''} var _html=''; $.each(data,function(i) { var row = data[i]; if (row.hasOwnProperty("Children")) { _html += '<li>'; _html += '<a href="#" class="dropdown-toggle">'; _html += '<i class="menu-icon fa ' + row.IconName + '"></i>'; _html += '<span class="menu-text nav-label">' + row.Name + '</span > '; _html += '<b class="arrow fa fa-angle-down"></b>'; _html += '</a >'; _html += '<b class="arrow"></b>'; _html += '<ul class="submenu">'; _html += getDom(row.Children); _html += '</ul>'; _html += '</li>'; } else { _html += '<li class="" id="' + row.Id + '">'; _html += '<a class="J_menuItem" href="' + row.Url + '">'; _html += '<i class="menu-icon fa ' + row.IconName + '"></i>'; _html += '<span class="menu-text">' + row.Name + '</span>'; _html += '</a>'; _html += '<b class="arrow"></b>'; _html += '</li>'; } }); return _html; };
(3)最后实现
$.ajax({ url: 'Api/Menu/GetTree', type: 'get', dataType: 'json', success: function (data) { var ltt = new LTT(data, { key_id: 'Id', key_parent: 'ParentId', key_child:'Children' }); var tree = ltt.GetTree(); console.log(tree); var html = getDom(tree); $("#side-menu").prepend(html); } })
附上ace官网地址
http://ace.jeka.by/index.html
第五节:SignalR大杂烩(与MVC融合、全局的几个配置、跨域的应用、C/S程序充当Client和Server)
一. 说在前面的话
本节主要在前面章节的基础上补充了几个简单的知识点,比如:第三方调用通过 GlobalHost.ConnectionManager.GetHubContext<MySpecHub1>();来获取Hub对象,那么能不能封装一下不必每次都这么获取呢?再比如SignalR传输是否有大小限制,一下传输10w个字能否传输成功?最后着重整理一下跨域的各种使用情况,结合C/S程序充当客户端和服务器端。
本节内容包括:
①. SignalR与MVC或者WebApi简单的整合。
②. 全局的几个配置。
③. 跨域的配置和应用。
④. C/S程序充当客户端或服务器端。
二. SignalR与MVC的简单整合
在前面的章节中我们已经知道,如果要通过控制器中的Action来实现通讯,需要通过 GlobalHost.ConnectionManager.GetHubContext<MySpecHub1>(); 来获取Hub类,但是每个Action中都这么获取,显得有点麻烦,这里简单封装一下,来便捷开发。
分析:实质在我们在Action中用到的对象无非也就这两个,IHubConnectionContext<dynamic> Clients 和 IGroupManager Groups ,所以这里利用继承的关系简单的封装一下,声明BaseController类,在里面获取这两个对象,然后其它控制器继承BaseController,并传入对应的Hub类,这样在Action中就可以直接使用 Clients和Groups了。
PS:WepAPI程序可以采用下面类似方式进行封装。
BaseController代码展示:
1 /// <summary> 2 /// 整合MVC和SignalR 3 /// </summary> 4 public class BaseController<T> : Controller where T : Hub 5 { 6 public IHubConnectionContext<dynamic> Clients { get; set; } 7 8 public IGroupManager Groups { get; set; } 9 10 public BaseController() 11 { 12 var hub = GlobalHost.ConnectionManager.GetHubContext<T>(); 13 Clients = hub.Clients; 14 Groups = hub.Groups; 15 } 16 }
继承BaseController的代码展示:
1 public class HubController : BaseController<MySpecHub1> 2 { 3 4 /// <summary> 5 /// 向所有人发送消息 6 /// </summary> 7 /// <param name="myConnectionId">当前用户的登录标记</param> 8 /// <param name="msg">发送的信息</param> 9 public string MySendAll(string myConnectionId, string msg) 10 { 11 //Hub模式 12 Clients.AllExcept(myConnectionId).receiveMsg($"用户【{myConnectionId}】发来消息:{msg}"); 13 return "ok"; 14 } 15 16 }
三. 全局的几个配置
这里的全局配置主要包括:传输超时时间、强制关闭时间、WebSocket模式下允许传输的数据最大值等等,以下配置代码可以在Configuration方法中进行配置,可以根据实际业务情况自行选择配置。
1. 表示客户端在转而使用其他传输或连接失败之前应允许连接的时间。默认值为 5 秒。(传输超时时间)
GlobalHost.Configuration.TransportConnectTimeout = TimeSpan.FromSeconds(5);
2. 表示连接在超时之前保持打开状态的时间
GlobalHost.Configuration.TransportConnectTimeout = TimeSpan.FromSeconds(5);
3. 用于表示在连接停止之后引发断开连接事件之前要等待的时间。 (强制关闭时间)
GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromSeconds(5);
4. 表示两次发送保持活动消息之间的时间长度。如果启用,此值必须至少为两秒。设置为 null 可禁用。
GlobalHost.Configuration.KeepAlive = TimeSpan.FromSeconds(2);
5. Websocket模式下允许传输数据的最大值,默认为64kb
GlobalHost.Configuration.MaxIncomingWebSocketMessageSize = 64;
四. 跨域的应用
在很多情况下,前后端是分离,客户端和服务器端并不在一个地址下,比如APP(这里指混合开发能使用JS的情况下),这个时候服务器的SignalR就需要配置允许跨域,这里有两种允许跨域的策略,一种是JSONP模式,另外一种是Cors模式。
在Startup类中的Configuration方法中进行配置,代码如下:
1 public class Startup 2 { 3 public void Configuration(IAppBuilder app) 4 { 5 //配置允许跨域 6 //1. JSONP模式 7 //app.MapSignalR(new HubConfiguration() { EnableJSONP = true }); 8 9 //2. Cors模式(需要安装Microsoft.Owin.Cors程序集) 10 app.UseCors(CorsOptions.AllowAll).MapSignalR(); 11 } 12 }
注:采用Cors模式的跨域需要安装:Microsoft.Owin.Cors 程序集,并且上述代码没有单独配置模型路径,所以采用的是默认路径“/signalr”。
当然前端代码也需要进行相应的改写:
(1). 代理模式的改写形式:
a. 自动生成代理类代码需要改写为 <script src="http://localhost:7080/signalr/hubs"></script> ,localhost:7080,根据实际情况改为实际地址。
b. 需要单独配置一下Hub的连接路径, conn.url = "http://localhost:7080/signalr";
以上两步即为全部改变,其余位置不需变化。
(2). 非代理模式下的代码:
非代理模式下就更容易,只需要在hubConnection方法中传入路径即可。如下图:
五. C/S程序充当客户端
C/S程序(这里采用控制台)充当客户端,当然服务器端必须已经配置了允许跨域,且C/S程序是没有JS的,所以只能采用非代理模式。
步骤如下:
1:安装程序集 Microsoft.AspNet.SignalR.Client
2:代码配置
a. 与服务器路径匹配的时候要注意,默认路径的话,要加上signalr/
b. 如果定义的方法大于一个参数的时候,需要声明一个类来接收
eg:Proxy.On<Person>("方法名", Person=>
Console.WriteLine("ID{0} Name{1}", Person.ID, Person.Name));
代码如下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 6 //一. 基础信息配置 7 //1. 与服务器路径进行匹配 8 var conn = new HubConnection("http://localhost:8099/signalr/"); 9 //2. 创建代理类 10 var proxy = conn.CreateHubProxy("MySpecHub1"); 11 12 13 //二. 定义客户端的方法 14 //特别注意,如果定义的方法大于一个参数的时候,msg的位置需要声明一个类来接受 15 //1 接受用户登录成功后的提示 16 17 proxy.On("LoginSuccessNotice", (msg) => 18 { 19 Console.WriteLine(msg); 20 }); 21 22 //2 接收自己的connectionId 23 proxy.On("ReceiveOwnCid", (msg) => 24 { 25 Console.WriteLine(msg); 26 }); 27 28 //三. 启动 29 conn.Start().Wait(); 30 31 Console.ReadKey(); 32 33 } 34 }
六. C/S程序充当服务器端
在很多情况下,我们需要避免使用IIS的性能开销,或者要将SignalR部署成Windows服务,这个使用就需要使用C/S程序作为服务器端了。
配置步骤比较简单,如下:
1. 安装程序集:Microsoft.AspNet.SignalR.SelfHost 和 Microsoft.Owin.Cors(跨域使用)
2. 添加集线器类MySpecHub1
3. 在Startup中配置允许跨域
4. 编写启动代码
PS:以上步骤2和步骤3在前面章节中已经多次提到过了,这里指展示一下启动代码:
1 static void Main(string[] args) 2 { 3 try 4 { 5 string url = "http://localhost:7080"; 6 using (WebApp.Start<Startup>(url)) 7 { 8 Console.WriteLine("Server running on {0}", url); 9 Console.ReadLine(); 10 } 11 } 12 catch (Exception ex) 13 { 14 Console.WriteLine(ex.Message); 15 } 16 Console.ReadKey(); 17 }
特别注意:如果报System.Reflection.TargetInvocationException was unhandled,直接去bin文件里以管理员身份运行exe程序即可或者以管理员身份运行VS程序然后启动即可。