WCF从理论到实践(13):事务投票

通过上文WCF从理论到实践:事务的学习,我们了解了WCF中实现事务的一些基本常识,但WCF中的事务并不止那么简单,上文中我们欠缺了一个最重要的功能:事务投票,所谓事务投票就是一种灵活控制事务提交的方式,在上文中我们设置服务方法的TransactionAutoComplete为true,其实意味着方法在没有异常的情况下自动投赞成票,但有时我们希望当操作中只有某个数据满足具体条件的时候,才能赞同事务提交,这样上文的实现明显就不满足需求了,此时我们可以用OperationContext.Current.SetTransactionComplete();显示的进行投票。注意,WCF的事务必须在全票通过的时候才能得以提交。本文还是结合银行的例子 来演示一下事务投票,并且搭配一个漂亮的WPF客户端,可谓买一送一了,:)。
本文目的
  • 进一步学习WCF事务
  • 顺便体验一下WPF
本文适合读者
本文适合WCF中级用户,至少需要了解事务的基本常识和简单实现,初学者可以先阅读WCF从理论到实践:事务

进一步学习WCF事务
本文中,我们要模拟的现实情境如下,搭建一个联盟银行服务自助系统,这个系统提供在各个银行之间进行自由转帐的功能,按照惯例,系统分为四个层次,分别如下:
                               
            层次
           
            项目
           
            服务契约
           
            Jillzhang.Wcf.Transactions.Contracts
           
            服务端
           
            Jillzhang.Wcf.Transactions
           
            宿主程序
           
            Jillzhang.Wcf.Transactions.ICBC-用于模拟工商银行
            Jillzhang.Wcf.Transactions.CCB-用于模拟建设银行
           
            客户端
           
            Jillzhang.Wcf.BankClient – 包括一个漂亮的WPF窗体
           

服务契约
我们在此处定义一个IBank的服务契约,它包括四个操作契约,分别为:



                               
            操作契约
           
            契约说明
           
[OperationContract(IsTerminating=false)]
            [TransactionFlow(TransactionFlowOption.Allowed)]
            decimal Receive(decimal money);
            接受其他银行的汇款,增加本行帐户余额
           
            [OperationContract(IsTerminating = false)] [TransactionFlow(TransactionFlowOption.Allowed)]
            decimal Send(decimal money);
           
            给其他银行帐户汇款,减少本行帐户余额
           
            [OperationContract(IsTerminating = false)]
            decimal GetBalance();
           
            获取帐户余额
           
            [OperationContract(IsTerminating=false)]
            decimal SendOnServer(decimal money,string toBank);
           
            通过一个银行接口,完成汇款操作,其中事务是在服务端进行
           

服务端
服务端,由于我本地没有数据库,偷下懒,就用一个静态变量表示帐户余额了。大家在验证事务的效果的时候,应该将这里的数据存储到数据库中。整个服务端的代码为:



服务端
//======================================================================

//

// Copyright (C) 2007-2008 Jillzhang

// All rights reserved

// guid1: 4990573c-d834-47f4-a592-3543a9dca047

// guid2: 3c1ffbfe-40e7-48b5-9f83-572dbdcb3bdd

// guid3: 8dfde0dc-07a6-41a9-8fc1-89a11593aa04

// guid4: 61ab2778-e017-4552-b70f-dc772f5e56f5

// guid5: e8c89ed4-360a-417b-a5b2-8e3a459733e3

// CLR版本: 2.0.50727.1433

// 新建项输入的名称: Class1

// 机器名称: JILLZHANG-PC

// 注册组织名:

// 命名空间名称: $rootnamespace$

// 文件名: Class1

// 当前系统时间: 3/31/2008 10:32:01 PM

// 用户所在的域: jillzhang-PC

// 当前登录用户名: jillzhang

// 创建年份: 2008

//

// created by Jillzhang at 3/31/2008 10:32:01 PM

//
http://jillzhang.cnblogs.com

//

//======================================================================



using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.ServiceModel;

using Jillzhang.Wcf.Transactions.Contracts;



namespace Jillzhang.Wcf.Transactions

{

[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete
=false,InstanceContextMode=InstanceContextMode.PerSession)]

public
class Bank:Contracts.IBank

{

static
decimal _balance = 10M;



public Bank()

{



}


[OperationBehavior(TransactionAutoComplete
=
false, TransactionScopeRequired =
true)]

public
decimal Receive(decimal money)

{

_balance
+= money;

Console.WriteLine(
"收到资金:"+money);

OperationContext.Current.SetTransactionComplete();

return _balance;

}


[OperationBehavior(TransactionAutoComplete
=
false, TransactionScopeRequired =
true)]

public
decimal Send(decimal money)

{

if (money > _balance)

{

throw
new FaultException("余额不足!");

}


_balance
-= money;

Console.WriteLine(
"发送资金:"
+ money);

OperationContext.Current.SetTransactionComplete();

return _balance;

}


public
decimal GetBalance()

{

return _balance;

}




public
decimal SendOnServer(decimal money,string toBank)

{

WSDualHttpBinding bind
=
new WSDualHttpBinding();

bind.TransactionFlow
=
true;

BankProxy bank
=
new BankProxy(bind, new EndpointAddress(toBank));

using (System.Transactions.TransactionScope tx =
new System.Transactions.TransactionScope())

{

if (money > _balance)

{

throw
new FaultException("余额不足!");

}


_balance
-= money;

Console.WriteLine(
"发送资金:"
+ money);

bank.Receive(money);

tx.Complete();

}


return _balance;

}


}


}



注意,本文的Receive和Send方法的TransactionAutoComplete设置的为false,这样就需要我们在事务包含的每个方法中均显示的进行事务投票,即调用OperationContext.Current.SetTransactionComplete();如果忽略此处,系统会出现如下的异常:


而且如果TransactionAutoComplete为false的时候,必须将InstanceContextMode设置为PerSession,并且服务契约(ServiceContract)也必须设置SessionMode为Required,否则分别 会出现如下异常:




而如果TransactionAutoComplete为True,则要求TransactionScopeRequired必须同时为True,否则会出现异常:


宿主程序
宿主程序非常简单,我们只是将服务寄宿到两个不同的进程中并且指定不同的服务地址便可,唯一值得注意的是因为服务契约需要事务支持,所以Binding的TransactionFlow也必须为True.
他们的代码分别为:
Jillzhang.Wcf.Transactions.ICBC

using System;



using System.Collections.Generic;



using System.Linq;



using System.Text;



using System.ServiceModel;



using Jillzhang.Wcf.Transactions.Contracts;







namespace Jillzhang.Wcf.Transactions.ICBC



{



class Program



{



static void Main(string[] args)



{



Uri baseAddress = new Uri("http://127.0.0.1:8654/ICBC");



using (ServiceHost host = new ServiceHost(typeof(Bank), baseAddress))



{



WSDualHttpBinding bind = new WSDualHttpBinding();



bind.TransactionFlow = true;



host.AddServiceEndpoint(typeof(IBank), bind, "");



host.Open();



Console.WriteLine("中国工商银行开始营业!");



Console.Read();



}



}



}



}




Jillzhang.Wcf.Transactions.CCB



CCB

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
[url]http://www.CodeHighlighter.com/[/url]

-->using System;



using System.Collections.Generic;



using System.Linq;



using System.Text;



using System.ServiceModel;



using Jillzhang.Wcf.Transactions.Contracts;







namespace Jillzhang.Wcf.Transactions.CCB



{



class Program



{



static void Main(string[] args)



{



Uri baseAddress = new Uri("http://127.0.0.1:8655/CCB");



using (ServiceHost host = new ServiceHost(typeof(Bank), baseAddress))



{



WSDualHttpBinding bind = new WSDualHttpBinding();



bind.TransactionFlow = true;



host.AddServiceEndpoint(typeof(IBank), bind, "");



host.Open();



Console.WriteLine("中国建设银行开始营业!");



Console.Read();



}



}



}



}


客户端 一个WPF应用程序,可以调用服务来模拟银行转帐功能,因为本文着重介绍WCF,对此不作详细赘述,以后在一起学WPF系列中会逐步加以学习,本文只给出主要的xaml文件和.cs文件
Window1.xaml

<Window x:Class="Jillzhang.Wcf.BankClient.Window1"



xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"



xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"



WindowStyle="ToolWindow" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen"



Title="银行联盟自助服务系统" ResizeMode="NoResize" Initialized="Window_Initialized">



<Window.Resources>



<Style x:Key="styleBackground">



<Setter Property="Control.Background">



<Setter.Value>



<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">



<GradientStop Color="#50000000" Offset="0.5"/>



<GradientStop Color="#ff999999" Offset="1"/>



</LinearGradientBrush>



</Setter.Value>



</Setter>



<Setter Property="Control.Width" Value="500">



</Setter>



</Style>



<!-- Banner Style -->



<Style x:Key="styleBanner">



<Setter Property="StackPanel.Background">



<Setter.Value>



<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">



<GradientStop Color="DarkGray" Offset="0.1" />



<GradientStop Color="Black" Offset="1" />



</LinearGradientBrush>



</Setter.Value>



</Setter>



<Setter Property="TextBlock.Foreground" Value="White" />



<Setter Property="TextBlock.FontFamily" Value="Tahoma" />



</Style>



<!-- CONTENT AREA STYLE -->



<Style x:Key="styleContentArea_Base">



<Setter Property="Border.BorderThickness" Value="1" />



<Setter Property="Border.CornerRadius" Value="10" />



<Setter Property="Border.Margin" Value="12" />



</Style>



<!-- Content Area Style -->



<Style x:Key="styleContentArea" BasedOn="{StaticResource styleContentArea_Base}">



<Setter Property="Border.Background" Value="White" />



<Setter Property="Border.BorderBrush" Value="Gray" />



<Setter Property="TextBlock.FontFamily" Value="Sans Serif" />



</Style>



</Window.Resources>



<Grid Style="{DynamicResource styleBackground}">



<Grid.RowDefinitions>



<RowDefinition Height="Auto"/>



<RowDefinition Height="*"/>



</Grid.RowDefinitions>



<Grid.ColumnDefinitions>



<ColumnDefinition Width="*"/>



<ColumnDefinition Width="2.5*"/>



</Grid.ColumnDefinitions>



<!-- BANNER -->



<Grid Grid.ColumnSpan="2" Grid.Column="0" Grid.Row="0" Height="70" Style="{DynamicResource styleBanner}">



<TextBlock



FontSize="26"



Padding="10,0,10,0"



Text="银行联盟自助服务系统"



VerticalAlignment="Center"



/>



</Grid>



<Grid Grid.Column="0" Grid.Row="1" Height="150">



<GroupBox Header="银行列表" FontSize="15" Margin="5,5,5,10">



<StackPanel Margin="5,20,5,15">



<RadioButton Name="radioButton1" FontSize="13" Height="25" IsChecked="True" Checked="radioButton1_Checked">中国工商银行</RadioButton>



<Separator Height="10"></Separator>



<RadioButton FontSize="13" Name="radioButton2" Height="25" Checked="radioButton2_Checked">中国建设银行</RadioButton>



</StackPanel>



</GroupBox>



</Grid>



<Grid Grid.Column="1" Grid.Row="1" Grid.RowSpan="2">



<Border Style="{DynamicResource styleContentArea}">



<Grid>



<Grid.RowDefinitions>



<RowDefinition Height="Auto" MinHeight="36" />



<RowDefinition Height="*"/>



<RowDefinition Height="*"/>



</Grid.RowDefinitions>



<Grid.ColumnDefinitions>



<ColumnDefinition Width="Auto" MinWidth="36" />



<ColumnDefinition Width="*"/>



</Grid.ColumnDefinitions>



<Image Margin="4,4,0,0" Stretch="None" Source="Resources\Icons\agent.ico" />



<StackPanel Grid.Column="1" Orientation="Horizontal">



<TextBlock FontSize="15" Padding="8" Text="您好,您操作的是:" VerticalAlignment="Center" />



<TextBlock FontSize="13" Padding="8" Name="AccountName" VerticalAlignment="Center" /> </StackPanel>



<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2">



<TextBlock Text="帐户余额:" FontSize="13" Padding="15"></TextBlock>



<TextBlock FontSize="13" Padding="8" Name="Balance" VerticalAlignment="Center" />



</StackPanel>



<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2" Margin="0,0,0,10">



<TextBlock Text="转帐金额:" FontSize="13" Padding="15" ></TextBlock>



<TextBox Text="" Width="100" Height="25" Name="textBox1"></TextBox>



<Separator Width="10" Height="0"></Separator>



<Button Content="转帐" Padding="25,2,25,2" Height="25" Click="Button_Click" Name="button1"></Button>



</StackPanel>



</Grid>



</Border>



</Grid>



</Grid>



</Window>




Window1.xaml.cs

Windows.xaml.cs

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
[url]http://www.CodeHighlighter.com/[/url]

-->using System;



using System.Collections.Generic;



using System.Linq;



using System.Text;



using System.Windows;



using System.Windows.Controls;



using System.Windows.Data;



using System.Windows.Documents;



using System.Windows.Input;



using System.Windows.Media;



using System.Windows.Media.Imaging;



using System.Windows.Navigation;



using System.Windows.Shapes;



using System.ServiceModel;



using System.Transactions;



using Jillzhang.Wcf.Transactions.Contracts;







namespace Jillzhang.Wcf.BankClient



{



/**//// <summary>



/// Interaction logic for Window1.xaml



/// </summary>



public partial class Window1 : Window



{



BankProxy bankICBC;



BankProxy bankCCB;



static readonly string icbcAddress = "http://127.0.0.1:8654/ICBC";



static readonly string ccbAddress = "http://127.0.0.1:8655/CCB";



bool inited = false;



public Window1()



{



InitializeComponent();



}



void InitAccountInfo()



{



if (!inited)



{



return;



}



try



{



if (radioButton1.IsChecked == true)



{



AccountName.Text = "中国工商银行";



Balance.Text = bankICBC.GetBalance().ToString();



}



else



{



AccountName.Text = "中国建设银行";



Balance.Text = bankCCB.GetBalance().ToString();



}



}



catch (Exception ex)



{



MessageBox.Show(ex.Message);



}



}







private void radioButton2_Checked(object sender, RoutedEventArgs e)



{



InitAccountInfo();



}







private void radioButton1_Checked(object sender, RoutedEventArgs e)



{



InitAccountInfo();



}







private void Window_Initialized(object sender, EventArgs e)



{



inited = true;



WSDualHttpBinding bind = new WSDualHttpBinding();



bind.TransactionFlow = true;



bankICBC = new BankProxy(bind, new EndpointAddress(icbcAddress));



bankCCB = new BankProxy(bind, new EndpointAddress(ccbAddress));



InitAccountInfo();



}







private void Button_Click(object sender, RoutedEventArgs e)



{



try



{



button1.IsEnabled = false;



decimal money = Convert.ToDecimal(textBox1.Text.Trim());



using (TransactionScope tx = new TransactionScope())



{



if (radioButton1.IsChecked == true)



{



Balance.Text = bankICBC.Send(money).ToString();



bankCCB.Receive(money);



}



else



{



Balance.Text = bankCCB.Send(money).ToString();



bankICBC.Receive(money);



}



tx.Complete();



MessageBox.Show("转帐操作完成!");



}



//if (radioButton1.IsChecked == true)



//{



// Balance.Text = bankICBC.SendOnServer(money, ccbAddress).ToString();



//}



//else



//{



// Balance.Text = bankCCB.SendOnServer(money, icbcAddress).ToString();



//}



}



catch (Exception ex)



{



MessageBox.Show(ex.Message);



}



finally



{



button1.IsEnabled = true;



textBox1.Text = "";



}



}



}



}


有两种方式可以实现转帐,上面代码中是在客户端实现事务,另外一种你可以将using (TransactionScope tx = new TransactionScope())块注释起来,然后将下面的注释取消,这样就可以在服务端实现事务。
下面就让我们来看下运行效果吧:
ICBC-工商行服务


CCB-建设行服务


我们漂亮的客户端效果为:


其实中国工商银行和中国建设银行的账户余额均为10,如上图所示,选择中国工商银行,然后输入转帐金额4,点击转帐,运行后效果如下:


选择中国建设银行,可以看到余额为14,如图:


范例项目下载
/Files/jillzhang/Jillzhang.Wcf.Transactions.rar
总结
WCF为我们提供了一种显示控制事务提交的方式,即事务投票,我们通过OperationContext.Current.SetTransactionComplete();来投赞成票,而只有参与事务的全部方法均投赞成票的时候,事务才能被成功提交。显示的投票方法比声明TransactionAutoComplet为true更灵活。同时我们又体验了一把WPF的魅力。







原文出处:http://www.cnblogs.com/jillzhang/