求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
SilverLight企业应用框架设计
 

作者: liulun ,发布于2012-10-25,来源:博客园

 

目录:

一.整体说明

二.框架画面

三.服务端设计

四.实体层设计+为客户端动态生成服务代理(自己实现RiaService)

五.客户端调用服务端(使用JSON传递数据,自己实现RESTful Web服务)

六.自定义系统菜单(使用自己的DataForm)

一.整体说明

下面我说一下这个框架的一些特点

一:自己实现基于HTTP的REST服务

用siliverlight开发企业应用

大多人数都喜欢用微软提供的EntityFramework + RIA Service

由于某些特殊的原因需要

项目可能不能用EF和RIAService那么这个系列将为您提供一些指导和最佳实践原则

二:自己实现客户端实体和服务代理的代码生成逻辑

使用RiaService的人肯定会知道,VS开发环境在Silverlight客户端,自动生成了服务代理的代码和实体的代码

这些代码放在Generated_Code\[YorRIAService].g.cs这个隐藏文件里

如果我们抛弃RIAService,那么势必将自己完成这些代码生成工作在我们这个系列中将介绍到每次编译程序都会自动生成这些代码的技巧

三:自己实现的DataForm控件

Silverlight提供的DataForm控件用以编辑实体

非常强大

但是太过强大了,把一些操作搞他复杂了

咱们这个系列将使用自己的DataForm控件

完成实体编辑业务逻辑

-------------------------------------

还有其他的一些诸如

自定义tabcontrol模版的技巧

都会在这个系列中讲到

二.框架画面

框架画面分为上中下三层

由下面一个Grid控件完成布局

    <Grid x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="60"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="22"></RowDefinition>
        </Grid.RowDefinitions>
       </Grid>

上层为顶部菜单区域

中层为子菜单和业务画面部分

下层为状态栏和版权信息区域

下面我们分别看一下这三个部分的生成逻辑

一:顶部菜单区域

XAML代码如下:

  <StackPanel x:Name="TopMenuS" Orientation="Horizontal" Background="{StaticResource HeadBG}">
     <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10"></ColumnDefinition>
            <ColumnDefinition Width="142"></ColumnDefinition>
            <ColumnDefinition Width="10"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="3"></RowDefinition>
            <RowDefinition Height="18"></RowDefinition>
            <RowDefinition Height="18"></RowDefinition>
            <RowDefinition Height="18"></RowDefinition>
            <RowDefinition Height="3"></RowDefinition>
        </Grid.RowDefinitions>
        <TextBlock x:Name="helloBlock" Grid.Column="1" Grid.Row="1" Text="xxxx通信" />
        <TextBlock x:Name="UserName" Text="Administrator" Grid.Column="1" Grid.Row="2" />
        <TextBlock Text="欢迎使用 xxx监控系统" Grid.Column="1" Grid.Row="3" />
     </Grid>
     <Rectangle Width="1" Fill="{StaticResource HeadSplitor}"></Rectangle>                        
     <StackPanel>
        <Button Style="{StaticResource ToolBtnStyle}" Content="全部关闭" Click="CloseAllClick"></Button>
        <Button Style="{StaticResource ToolBtnStyle}" Content="关闭当前" Click="CloseCurClick"></Button>
        <Button Style="{StaticResource ToolBtnStyle}" Content="关闭其他" Click="CloseOtherClick"></Button>
     </StackPanel>
     <Rectangle Width="1" Fill="{StaticResource HeadSplitor}"></Rectangle>
     <StackPanel>
         <Button Style="{StaticResource ToolBtnStyle}" Content="修改密码" Click="ChangePSWBtnClick"></Button>
         <Button Style="{StaticResource ToolBtnStyle}" Content="退出系统" Click="LoginOutBtnClick"></Button>
         <Button Style="{StaticResource ToolBtnStyle}" Content="重新登录" Click="ReLoginBtnClick"></Button>
     </StackPanel>
     <Rectangle Width="1" Fill="{StaticResource HeadSplitor}"></Rectangle>
 </StackPanel> 

顶部菜单分为三部分

从左向右依次是

欢迎信息(GRID)

顶部菜单(动态创建)

常用按钮(StackPanel)

顶部菜单的容器是一个StackPanel

此容器有一个渐变的背景色,样式代码如下

    <!--头部渐变背景-->
    <LinearGradientBrush x:Key="HeadBG" StartPoint="0.5 0" EndPoint="0.5 1">
        <GradientStop Offset="0" Color="#FAFAFA"></GradientStop>
        <GradientStop Offset="0.5" Color="#D6D6D6"></GradientStop>
    </LinearGradientBrush>

此容器每个部分都有一个Rectangle来分割

此Rectangle也有个渐变的背景,代码如下

    <!--头部分隔条渐变背景-->
    <LinearGradientBrush x:Key="HeadSplitor" StartPoint="0.5 0" EndPoint="0.5 1">
        <GradientStop Offset="0" Color="#FAFAFA"></GradientStop>
        <GradientStop Offset="1" Color="#000000"></GradientStop>
    </LinearGradientBrush> 

常用按钮的样式如下:

<!--头部的 三分栏  工具按钮样式-->
  <Style x:Key="ToolBtnStyle" TargetType="Button">
     <Style.Setters>
       <Setter Property="Width" Value="90"></Setter>
       <Setter Property="Height" Value="20"></Setter>
       <Setter Property="Cursor" Value="Hand"></Setter>
       <Setter Property="Template">
          <Setter.Value>
              <ControlTemplate TargetType="Button">
                  <Grid x:Name="Container">
                      <vsm:VisualStateManager.VisualStateGroups>
                          <vsm:VisualStateGroup x:Name="CommonStates">
                              <vsm:VisualState x:Name="MouseOver">
                                 <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="fillColor" 
Storyboard.TargetProperty="Opacity" Duration="0" To="1"/>
                                 </Storyboard>
                              </vsm:VisualState>
                          <vsm:VisualState x:Name="Normal" />
                       </vsm:VisualStateGroup>
                   </vsm:VisualStateManager.VisualStateGroups>
                   <Rectangle x:Name="fillColor" Opacity="0" Fill="#B5B5B5"  IsHitTestVisible="False" RadiusX="1" 
RadiusY="1"/>
                   <ContentPresenter                                
    			        x:Name="contentPresenter"
    			        Content="{TemplateBinding Content}"
    			        ContentTemplate="{TemplateBinding ContentTemplate}"
    			        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
    			        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
    			        Margin="{TemplateBinding Padding}"/>
                   </Grid>
                </ControlTemplate>
             </Setter.Value>
        </Setter>
     </Style.Setters>
  </Style> 

动态创建顶部菜单的代码如下

(大家先不要深究数据是怎么来的,在以后的章节咱们会讲到数据交互的细节)

   var tops = Common.ViewUtility.AllMenu
      .Where(m => m.ParentId == Guid.Empty)
      .OrderByDescending(m => m.MenuOrder);
   foreach (var m in tops)
   {
      var topM = new HeadBtn();
      topM.DataContext = m;
      topM.MouseLeftButtonUp += new MouseButtonEventHandler(topM_MouseLeftButtonUp);
      TopMenuS.Children.Insert(2,topM);
   }

这里创建的HeadBtn是一个自定义的控件

(每个顶部菜单就是一个控件的实例)

该自定义控件XAML代码如下:

    <StackPanel Orientation="Horizontal" Cursor="Hand">
        <Grid x:Name="btn" Width="90" Height="60">
            <Grid.RowDefinitions>
                <RowDefinition></RowDefinition>
                <RowDefinition Height="20"></RowDefinition>
            </Grid.RowDefinitions>
            <Image Source="../Images/module2.png" Height="30"></Image>
            <TextBlock Text="{Binding MenuName}" Grid.Row="1" FontSize="12"
                       VerticalAlignment="Center" HorizontalAlignment="Center" ></TextBlock>
        </Grid>
        <Rectangle Width="1" Fill="{StaticResource HeadSplitor}"></Rectangle>
    </StackPanel> 

注意,这里每个顶部菜单的ICO图标不是动态的,朋友们,想让他变成动态的就自己动手吧

为了实现美观的效果

我为这个自定义控件定义了鼠标的滑入滑出事件

    private void UserControl_MouseEnter(object sender, MouseEventArgs e)
    {
        var color = Color.FromArgb(255,180, 180, 180);
         btn.Background = new SolidColorBrush(color);
    }
    private void UserControl_MouseLeave(object sender, MouseEventArgs e)
    {
        btn.Background = new SolidColorBrush(Colors.Transparent);
    }

这些颜色的值,本应该作为资源放在样式文件中,我这里也没有做处理

二:底部状态条区域

此处比较简单

代码如下:

   <StackPanel Width="Auto" Grid.Row="2" 
         Background="#B5B5B5" Orientation="Horizontal" FlowDirection="RightToLeft">
      <TextBlock VerticalAlignment="Center" Text="V1.0.0 Copy Right ? All Rights Reserved"/>
      <TextBlock VerticalAlignment="Center"  Text="xxxx"/>
   </StackPanel>

1.我没有做状态信息的内容

2.版本信息应该通过Assambly获取

三:中部区域

XAML代码如下

  <Border  Grid.Row="1" BorderBrush="#B5B5B5" BorderThickness="0 1 0 0">
     <Grid  Background="#E8E8E8" Margin="0 1 0 0" Name="CenterGrid">
        <Grid.ColumnDefinitions>
           <ColumnDefinition Width="160"></ColumnDefinition>
           <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <StackPanel>
           <sdk:Label  Height="26"  Background="#b5b5b5"  Margin="0 6 0 6"
                  FontWeight="Bold" FontSize="12" x:Name="lblMenuText" 
                  HorizontalAlignment="Center"  Width="160"  />
           <ListBox  SelectionChanged="left_panel_SelectionChanged"  
                  ItemContainerStyle="{StaticResource ListBoxItemStyleNew}"
                  Grid.Column="0" x:Name="left_panel" Background="#E8E8E8" BorderThickness="0">
           <ListBox.ItemTemplate>
               <DataTemplate>
                   <ContentPresenter Content="{Binding MenuName}"></ContentPresenter>
               </DataTemplate>
           </ListBox.ItemTemplate>
            </ListBox>
        </StackPanel>
        <sdk:TabControl x:Name="tbControl" SelectionChanged="tbControl_SelectionChanged" 
Grid.Column="1" Margin="0 6 0 0">
        </sdk:TabControl>
     </Grid>
   </Border>

其中Label控件显示的为顶部菜单的标题,标志着当前选中的是哪个顶部菜单

ListBox为子菜单控件

TabControl为业务画面区域

四:子菜单区域

子菜单样式相对复杂些

样式代码如下

<!--子菜单样式-->
  <Style TargetType="ListBoxItem" x:Key="ListBoxItemStyleNew" >
     <Setter Property="Padding" Value="3" />
     <Setter Property="HorizontalContentAlignment" Value="Center" />
     <Setter Property="VerticalContentAlignment" Value="Center" />
     <Setter Property="Background" Value="Transparent" />
     <Setter Property="BorderThickness" Value="1"/>
     <Setter Property="TabNavigation" Value="Local" />
     <Setter Property="Template">
         <Setter.Value>
            <ControlTemplate TargetType="ListBoxItem">
               <Grid Background="{TemplateBinding Background}">
                   <vsm:VisualStateManager.VisualStateGroups>
                      <vsm:VisualStateGroup x:Name="CommonStates">
                         <vsm:VisualState x:Name="Normal" />
                            <vsm:VisualState x:Name="MouseOver">
                              <Storyboard>
                                 <DoubleAnimation Storyboard.TargetName="fillColor" Storyboard.TargetProperty=
"Opacity" Duration="0" To=".35"/>
                              </Storyboard>
                              </vsm:VisualState>
                              <vsm:VisualState x:Name="Disabled">
                                 <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.
TargetProperty="Opacity" Duration="0" To=".55" />
                                 </Storyboard>
                              </vsm:VisualState>
                          </vsm:VisualStateGroup>
                          <vsm:VisualStateGroup x:Name="SelectionStates">
                              <vsm:VisualState x:Name="Unselected" />
                              <vsm:VisualState x:Name="Selected">
                                <Storyboard>
                                   <DoubleAnimation Storyboard.TargetName="fillColor2" Storyboard.TargetProperty=
"Opacity" Duration="0" To=".75"/>
                                </Storyboard>
                             </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="FocusStates">
                             <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                   <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisualElement" 
Storyboard.TargetProperty="Visibility" Duration="0">
                                       <DiscreteObjectKeyFrame KeyTime="0">
                                          <DiscreteObjectKeyFrame.Value>
                                              <Visibility>Visible</Visibility>
                                           </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="Unfocused"/>
                        </vsm:VisualStateGroup>
                     </vsm:VisualStateManager.VisualStateGroups>
                     <Rectangle x:Name="fillColor" Opacity="0" Fill="#B5B5B5"  IsHitTestVisible="False" RadiusX="1" 
RadiusY="1"/>
                     <Rectangle x:Name="fillColor2" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" 
RadiusY="1"/>
                     <ContentPresenter
                         x:Name="contentPresenter"
                             Cursor="Hand"
                          Content="{TemplateBinding Content}"
                          ContentTemplate="{TemplateBinding ContentTemplate}"
                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                          Margin="{TemplateBinding Padding}"/>
                      <Rectangle x:Name="FocusVisualElement" Stroke="#FF6DBDD1" StrokeThickness="1" Visibility=
"Collapsed" RadiusX="1" RadiusY="1" />
                 </Grid>
             </ControlTemplate>
         </Setter.Value>
     </Setter>
   </Style> 

这些样式主要是为了实现如下效果

子菜单数据绑定非常简单

(顶部菜单的单击事件将绑定子菜单)

代码如下:

        void topM_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            var tBTN = sender as HeadBtn;
            var tMenu = tBTN.DataContext as MenuM;
            lblMenuText.Content = tMenu.MenuName;
            var subs = Common.ViewUtility.AllMenu
                .Where(m => m.ParentId == tMenu.Id)
                .OrderBy(m => m.MenuOrder);
            left_panel.ItemsSource = subs;
        }

五:业务画面区域

业务画面的容器为TabControl

每个TabItem将承载一个业务画面

主要是为TabItem增加关闭按钮

XAML代码如下:

    <sdk:TabItem.HeaderTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal" Width="auto" Margin="0 0 -2 -2">
                <TextBlock  x:Name="tboxheader" Text="{Binding}"/>
                <Button Cursor="Hand" Click="CloseBTN_Click" Style="{StaticResource
 ListViewHeadBtnStyle}" Margin="3,-3,-6,0"  Content="X" >
                </Button>
            </StackPanel>
        </DataTemplate>
    </sdk:TabItem.HeaderTemplate>
    </sdk:TabItem> 

这个关闭按钮的样式比较特殊

    <!--标签按钮-->
    <Style x:Key="ListViewHeadBtnStyle" TargetType="Button">
        <Style.Setters>
            <Setter Property="Width" Value="20"></Setter>
            <Setter Property="Height" Value="20"></Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Grid  x:Name="Container">
                            <vsm:VisualStateManager.VisualStateGroups>
                                <vsm:VisualStateGroup x:Name="CommonStates">
                                    <vsm:VisualState x:Name="Normal" />
                                    <vsm:VisualState x:Name="MouseOver">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="fillColor" Storyboard.
TargetProperty="Opacity" Duration="0" To=".35"/>
                                        </Storyboard>
                                    </vsm:VisualState>
                                </vsm:VisualStateGroup>
                            </vsm:VisualStateManager.VisualStateGroups>
                            <Rectangle x:Name="fillColor" Opacity="0" Fill="#B5B5B5"  IsHitTestVisible="False" 
RadiusX="1" RadiusY="1"/>
                            <ContentPresenter                                
    			                x:Name="contentPresenter"
    			                Content="{TemplateBinding Content}"
    			                ContentTemplate="{TemplateBinding ContentTemplate}"
    			                VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
    			                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
    			                Margin="{TemplateBinding Padding}"/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style.Setters>
    </Style>

关闭按钮样式主要为了实现如下效果

(鼠标滑上,关闭按钮背景变灰色)

按钮的单击事件如下

        private void CloseBTN_Click(object sender, RoutedEventArgs e)
        {
            var tc = this.Parent as TabControl;
            tc.Items.Remove(this);
        }

三.服务端设计

一:缓存服务类型与方法

客户端请求的时候

为了方便的知道请求的类型与类型所包含的方法

我们把服务类型和方法缓存到静态字典中了

代码如下

    public class WCFRouteTable
    {
        static Dictionary<string, Type> routeService;
        static Dictionary<string, MethodInfo> routeMethods;
        static WCFRouteTable()
        {
            routeService = new Dictionary<string, Type>();
            routeMethods = new Dictionary<string, MethodInfo>();
            var ass = (typeof(WCFRouteTable)).Assembly;
            var ts = ass.GetTypes();
            foreach (var t in ts)
            {
                if (t.FullName.StartsWith("RTMDemo.Host.WCF"))
                {
                    routeService.Add(t.FullName, t);
                    foreach (var m in t.GetMethods())
                    {
                        var mName = string.Format("{0}.{1}", t.FullName, m.Name);
                        routeMethods.Add(mName, m);
                    }
                }
            }
        }
        public static Type GetWCFType(string key)
        {
            Type result = null;
            if (routeService.ContainsKey(key))
            {
                result = routeService[key];
            }
            return result;
        }
        public static MethodInfo GetMethodInfo(string key)
        {
            MethodInfo result = null;
            if (routeMethods.ContainsKey(key))
            {
                result = routeMethods[key];
            }
            return result;
        }

    } 

二:托管HTTP请求

在webconfig中增加module以托管请求

    <modules>
            <add name="WcfHttpModule" type="RTMDemo.Host.WCFHttpModule, RTMDemo.Host"/>
    </modules>

托管请求对应的类的代码如下

    public class WCFHttpModule:IHttpModule
    {
        public void Dispose() { }
        /// <summary>
        /// 托管请求
        /// </summary>
        /// <param name="context"></param>
        public void Init(HttpApplication context)
        {
            context.BeginRequest += (sender, args) =>
            {
                string relativeAddress = HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath.Remove(0, 2);
                Type serviceType = WCFRouteTable.GetWCFType(relativeAddress);
                if (null == serviceType)
                {
                    return;
                }
                IHttpHandler handler = new WCFHandler(serviceType);
                context.Context.RemapHandler(handler);
            };
        }
    } 

通过这行代码

Type serviceType = WCFRouteTable.GetWCFType(relativeAddress);

用户只要请求如下路径

http://localhost/RTMDemo.Host/RTMDemo.Host.WCF.MenuService

就会得到MenuService的类型

然后把服务类型传给指定的处理程序

三:处理请求

在WCFHandler类中最重要的莫过于

处理请求的方法

代码如下

/// <summary>
        /// 处理请求
        /// </summary>
        /// <param name="context"></param>
        public void ProcessRequest(HttpContext context)
        {
            try
            {
                List<object> paramList = new List<object>();
                JavaScriptSerializer jss = new JavaScriptSerializer();
                var MethodKey = context.Request["MethodKey"];
                var minfo = WCFRouteTable.GetMethodInfo(MethodKey);
                var si = new MethodInvoker(minfo);
                ParameterInfo[] ps = minfo.GetParameters();
                var pstrs = context.Request.Form.AllKeys.OrderBy(m=>m).ToArray();
                var pIndex = 0;
                for(var i=0;i<pstrs.Length;i++)
                {
                    if (string.IsNullOrEmpty(pstrs[i]))
                    {
                        continue;
                    }
                    if (pstrs[i].StartsWith("p"))
                    {
                        var pStr = context.Request[pstrs[i]];
                        var obj = jss.Deserialize<object>(pStr);
                        var bts = Encoding.UTF8.GetBytes(pStr);
                        MemoryStream mss = new MemoryStream(Encoding.UTF8.GetBytes(pStr));
                        DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(ps[pIndex].
ParameterType); 
                        var p = jsonSerializer.ReadObject(mss);
                        paramList.Add(p);
                        pIndex += 1;
                    }
                }

                //todo:此处性能不佳
                var instance = Activator.CreateInstance(ServiceType);
                var result = si.Execute(instance,paramList.ToArray());
                var ss = jss.Serialize(result);

                context.Response.ClearContent();
                context.Response.ContentEncoding = Encoding.UTF8;
                context.Response.ContentType = "application/json; charset=utf-8";
                context.Response.Write(ss);
                context.Response.Flush();
            }
            catch
            {
                context.Response.Write("我们不提供此服务的元数据~<br />");
                context.Response.Write("@@@@@@~<br />@@@@@@@~");
                return;
            }
        }
    }

注意:首先说这段代码还有很大的优化空间;也未经过严格的测试;但思路基本就是这样的

处理请求主要做了如下几步工作:

1.先根据请求POST上来的信息得到准备执行的方法

var MethodKey = context.Request["MethodKey"];

var minfo = WCFRouteTable.GetMethodInfo(MethodKey);

MethodInvoker稍后再讲

2.按顺序取出了方法的参数,并用DataContractJsonSerializer反序列化成对象

方法参数都是用JSON字符串传递的

3.通过反射创建了服务的实例

然后调用该实例的方法

得到方法的返回值,并序列化成JSON字符串

4.把返回值以JSON的形式输出给客户端

四:其他

1.MethodInvoker是用的老赵的类;具体是哪篇文章,我已经找不到了。

public class MethodInvoker
    {
        private Func<object, object[], object> m_execute;

        public MethodInvoker(MethodInfo methodInfo)
        {
            this.m_execute = this.GetExecuteDelegate(methodInfo);
        }

        public object Execute(object instance, params object[] parameters)
        {
            return this.m_execute(instance, parameters);            
        }

        private Func<object, object[], object> GetExecuteDelegate(MethodInfo methodInfo)
        {
            ParameterExpression instanceParameter = Expression.Parameter(typeof(object), "instance");
            ParameterExpression parametersParameter = Expression.Parameter(typeof(object[]), "parameters");

            List<Expression> parameterExpressions = new List<Expression>();
            ParameterInfo[] paramInfos = methodInfo.GetParameters();
            for (int i = 0; i < paramInfos.Length; i++)
            {
                var ce = Expression.Constant(i);
                BinaryExpression valueObj = Expression.ArrayIndex(parametersParameter,ce);
                UnaryExpression valueCast = Expression.Convert(valueObj, paramInfos[i].ParameterType);
                parameterExpressions.Add(valueCast);
            }
            var instanceE = Expression.Convert(instanceParameter, methodInfo.ReflectedType);
            Expression instanceCast = methodInfo.IsStatic ? null : instanceE;
            MethodCallExpression methodCall = Expression.Call(instanceCast, methodInfo, parameterExpressions);
            if (methodCall.Type == typeof(void))
            {
                Expression<Action<object, object[]>> lambda = Expression.Lambda<Action<object, 
object[]>>(methodCall, instanceParameter, parametersParameter);
                Action<object, object[]> execute = lambda.Compile();
                return (instance, parameters) =>
                {
                    execute(instance, parameters);
                    return null;
                };
            }
            else
            {
                UnaryExpression castMethodCall = Expression.Convert(methodCall, typeof(object));
                Expression<Func<object, object[], object>> lambda =Expression.Lambda<Func<object, object[], 
object>>(castMethodCall, instanceParameter, parametersParameter);
                return lambda.Compile();
            }
        }
    }

2.服务类和数据访问的类没有什么特殊的

我这里只公布一个服务的类

    public class MenuService
    {
        public List GetAllMenu()
        {            
            using (var DA = new MenuDA())
            {
                var result = DA.GetAllMenu();
                return result;
            }
        }
        public void DelMenu(Guid Id)
        {
            using (var DA = new MenuDA())
            {
                DA.DelMenu(Id);
            }
        }
        public void AddMenu(MenuM m)
        {
            using (var DA = new MenuDA())
            {
                DA.AddMenu(m);
            }
        }
        public void UpdateMenu(MenuM m)
        {
            using (var DA = new MenuDA())
            {
                DA.UpdateMenu(m);
            }
        }
    }

MenuDa就是数据访问类了

很普通,就不在公布代码了

3.完成这些工作之后

我们只要在客户端构造好表单

然后把表单POST到指定的路径

就能完成服务的访问了!

四.实体层设计+为客户端动态生成服务代理(自己实现RiaService)

说个前提条件:

此项目虽然使用了silverlight 4.0

但是服务端只能在dotNet3.5下运行

这也是我们为什么自己实现riaService的原因

实体层设计

由于有这个限制条件,我们设计的实体层也有所区别

如下图为实体层的程序集(只有MenuM实体类,其他实体类未加入。)

下面来看一下实体层MenuM的代码

namespace RTMDemo.Model
{
    [DataContract]
    public class MenuM : Entity
    {
        public string _MenuName;
        public string _Url;
        public string _MenuDes;
        public int _OrderNum;
        [DataMember]        
        public Guid Id { get; set; }
        [DataMember]
        [Display(Name = "菜单名称")]
        [Required(ErrorMessage="不能为空")]
        public string MenuName
        {
            get
            {
                return _MenuName;
            }
            set
            {
                ValidateProperty("MenuName", value);
                _MenuName = value;
            }
        }
        [DataMember]
        public Guid ParentId { get; set; }
        [DataMember]
        [Display(Name = "菜单路径")]
        [Required(ErrorMessage = "不能为空")]
        public string Url
        {
            get
            {
                return _Url;
            }
            set
            {
                ValidateProperty("Url", value);
                _Url = value;
            }
        }
        [DataMember]
        [Display(Name = "菜单帮助")]
        public string MenuDes
        {
            get
            {
                return _MenuDes;
            }
            set
            {
                ValidateProperty("Help", value);
                _MenuDes = value;
            }
        }
        [DataMember]
        [Display(Name = "菜单排序")]
        [RegularExpression("[1-9]+[0-9]", ErrorMessage = "只能输入数字")]
        public int OrderNum
        {
            get
            {
                return _OrderNum;
            }
            set
            {
                ValidateProperty("MenuName", value);
                _OrderNum = value;
            }
        }
    }
}

这里有几点需要说明

1:特性[DataContract]与[DataMember]标记,是为了客户端与服务端传输数据的时候序列化与反序列化引入的

2:MenuM类继承自Entity类

然而在.net 3.5中是没有Entity类的

那么我们就创建了这个类(就是Attr文件夹下的 Entity.cs类)

namespace System.ServiceModel.DomainServices.Client
{
    [DataContract]
    public class Entity
    {
        public void ValidateProperty(string PropertyName, object value)
        {
        }
    }
}

这个类虽然在这里看上去没什么用

但是在silverlight客户端用处就非常大(等会会说道为silverlight客户端自动生成实体类型,silverlight 4.0是有Entity类的)

3:[Display(Name = "菜单名称")]

如上:Display特性在dotNet3.5中也是不存在的

同理,我们创建了DisplayAttribute特性,也是为了使用Silverlight4.0的客户端特性

namespace System.ComponentModel.DataAnnotations
{
    public sealed class DisplayAttribute : Attribute
    {
        public string Name { get; set; }
    }
}

为客户端动态生成服务代理和实体类型

使用过Silverlight RIA Service的人一定都知道

每次编译的时候都会在Silverlight程序集中生成如下目录和文件

此文件就包含了服务代理和实体类型

那么为了达到与RIA Service一样的效果

我们为服务端程序集增加了VS2010的后期生成事件命令行

如下图所示

命令行代码为$(SolutionDir)RTMDemo.Compile\bin\Debug\RTMDemo.Compile.exe

其中$(SolutionDir)为宏,指解决方案的目录(定义为驱动器 + 路径);包括尾部的反斜杠“\”。

更多生成事件命令行的宏请参见这里:http://msdn.microsoft.com/zh-cn/library/42x5kfw4(v=vs.90).aspx

这个命令行的意思是

在编译完服务端类库后

执行RTMDemo.Compile\bin\Debug\RTMDemo.Compile.exe

也就是这个类库的生成文件

那么我们来看一下这个程序集中的主要工作

1.保存目录路径以备读取和写入

        [DllImport("Kernel32.dll")]
        public static extern string GetCommandLineA();
        //实体的路径
        static string mPath;
        //服务的路径
        static string sPath;
        //客户端的路径
        static string tarPath;
        [STAThread]
        static void Main()
        {
            var rtstr = @"RTMDemo.Compile\bin\Debug\RTMDemo.Compile.exe";
            //获取命令行参数
            var arg = GetCommandLineA().Replace("\"","");
            mPath = arg.Replace(rtstr,"RTMDemo.Model");
            sPath = arg.Replace(rtstr, @"RTMDemo.Host\bin\RTMDemo.Host.dll");
            tarPath = arg.Replace(rtstr, @"RTMDemo.Frame\Generated_Code\RTMDemo.Host.g.cs");

            AddModels();
            AddService();
        }

2.添加实体类型

        static void AddModels()
        {
            var di = new DirectoryInfo(mPath);
            var sb = new StringBuilder(Environment.NewLine);
            foreach (var fi in di.GetFiles())
            {
                var pname = Path.GetFileNameWithoutExtension(fi.Name);
                if (!pname.EndsWith("M"))
                {
                    continue;
                }
                var sr = new StreamReader(fi.FullName);
                var content = sr.ReadToEnd();
                var matche = Regex.Match(content, @"\[DataContract(.|\n)*}");
                var str = matche.Value;
                str = str.TrimEnd('}');
                sb.Append(Environment.NewLine);
                sb.AppendFormat("    {0}", str);
                sr.Close();
            }
            sb.Append(Environment.NewLine);
            WriteToTar("实体", sb.ToString());
        }

此端代码大意为:

遍历实体类库文件夹内的文件,

读取文件名以M结尾的文件(约定实体类名必须以M结尾)

然后按正则匹配[DataContract]以后的内容

把这些内容保存起来以备写入目标文件

3.添加服务代理

       static void AddService()
        {
            
            var ass = Assembly.LoadFile(sPath);
            var types = ass.GetTypes();
            var sb = new StringBuilder(Environment.NewLine);
            foreach (var ty in types)
            {
                if (!ty.Name.EndsWith("Service"))
                {
                    continue;
                }
                sb.AppendLine(string.Format("public class {0}", ty.Name));
                sb.AppendLine("{");
                sb.AppendLine("public event ServiceEventHandler Completed;");
                var methods = ty.GetMethods();
                foreach (var me in methods)
                {
                    if (me.DeclaringType != ty)
                    {
                        continue;
                    }
                    sb.AppendFormat("public void {0}(", me.Name);
                    var Parameters = me.GetParameters();
                    var ps = new StringBuilder();
                    for (var i = 0; i < Parameters.Length; i++)
                    {
                        var tn = GetTypeName(Parameters[i].ParameterType);
                        var dh = (i == Parameters.Length - 1?"":",");
                        sb.AppendFormat("{0} {1}{2}", tn, Parameters[i].Name,dh);
                        ps.AppendFormat(" ,{0}", Parameters[i].Name);
                    }
                    sb.AppendFormat("){0}", Environment.NewLine);
                    sb.AppendLine("{");
                    sb.AppendLine("var si = new ServiceInvoker();");
                    sb.AppendLine("si.Completed += new ServiceEventHandler(si_Completed);");
                    var rtp = GetTypeName(me.ReturnType);
                    if (rtp == "Void")
                    {
                        sb.AppendLine(string.Format("si.PrepareInvoke(\"{0}\", \"{1}\", {2} {3});",
                            ty.Name, me.Name, "null", ps.ToString()));
                    }
                    else
                    {
                        sb.AppendLine(string.Format("si.PrepareInvoke(\"{0}\", \"{1}\", typeof({2}) {3});", 
                                    ty.Name, me.Name, rtp, ps.ToString()));
                    }
                    sb.AppendLine("si.InvokeService();");
                    sb.AppendLine("}");
                }
                sb.AppendLine("void si_Completed(object sender, ServiceEventArgs e)");
                sb.AppendLine("{");
                sb.AppendLine("Completed(sender, e);");
                sb.AppendLine("}");
                sb.AppendLine("}");
            }
            sb.Append(Environment.NewLine);
            WriteToTar("服务", sb.ToString());
        }

获取服务端类信息与获取实体类信息不同

获取服务端类信息使用了反射

我们反射出类的名字,类中的方法名,参数名,参数类型,返回值类型等

来生成形如下面这样的服务端代理

    public class MenuService
    {
        public event ServiceEventHandler Completed;
        public void GetAllMenu()
        {
            var si = new ServiceInvoker();
            si.Completed += new ServiceEventHandler(si_Completed);
            si.PrepareInvoke("MenuService", "GetAllMenu", typeof(List));
            si.InvokeService();
        }
        public void DelMenu(Guid Id)
        {
            var si = new ServiceInvoker();
            si.Completed += new ServiceEventHandler(si_Completed);
            si.PrepareInvoke("MenuService", "DelMenu", null, Id);
            si.InvokeService();
        }
        public void AddMenu(MenuM m)
        {
            var si = new ServiceInvoker();
            si.Completed += new ServiceEventHandler(si_Completed);
            si.PrepareInvoke("MenuService", "AddMenu", null, m);
            si.InvokeService();
        }
        public void UpdateMenu(MenuM m)
        {
            var si = new ServiceInvoker();
            si.Completed += new ServiceEventHandler(si_Completed);
            si.PrepareInvoke("MenuService", "UpdateMenu", null, m);
            si.InvokeService();
        }
        void si_Completed(object sender, ServiceEventArgs e)
        {
            Completed(sender, e);
        }
    } 

至于ServiceInvoker是什么,我们将在下一节内容中介绍

注意:这样生成服务端代理暂不支持生成服务端方法的重载代理

在获取参数或返回值类型的时候,

会遇到获取泛型类型的情况(如:List~<….>,就不能把这些拼到字符串内去)

我们使用诸如下面的函数来做简单判别

        public static string GetTypeName(Type t)
        {
            if (t.Name.StartsWith("List"))
            {
                var gtype = t.GetGenericArguments().FirstOrDefault();
                var result = string.Format("List<{0}>", gtype.Name);
                return result;
            }
            return t.Name;
        }

4.写入代理文件

原始的代理文件模版如下

//------------------------------------------------------------------------------
// 
//     此代码由工具生成。
//     对此文件的更改可能会导致不正确的行为,
//     并且如果重新生成代码,这些更改将会丢失。
// 
//------------------------------------------------------------------------------

namespace RTMDemo.Frame.Client
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.Serialization;
    using RTMDemo.Frame.Common;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel;
    using System.ServiceModel.DomainServices.Client;
    #region 实体

    #endregion

    #region 服务

    #endregion
}

写入代理文件主要是匹配两个region

然后插入之前生成的字符串

        static void WriteToTar(string reg, string content)
        {
            reg = string.Format(@"(?<=#region {0})(.|\n)*?(?=#endregion)", reg);
            var tsr = new StreamReader(tarPath);
            var tcontent = tsr.ReadToEnd();
            tsr.Close();
            tcontent = Regex.Replace(tcontent, reg, content);
            var tarStream = new StreamWriter(tarPath);
            tarStream.Write(tcontent);
            tarStream.Close();
        }

调用此函数的代码如下

WriteToTar("服务", sb.ToString());

至此就把服务代理和实体类型都拷贝到客户端去了

在下一节我们介绍怎么使用这些内容

五.客户端调用服务端(使用JSON传递数据,自己实现RESTful Web服务)

在上一节中讲到的自动生成的服务代理类核心代码,如下

        public event ServiceEventHandler Completed;
        public void GetAllMenu()
        {
            var si = new ServiceInvoker();
            si.Completed += new ServiceEventHandler(si_Completed);
            si.PrepareInvoke("MenuService", "GetAllMenu", typeof(List));
            si.InvokeService();
        }
        void si_Completed(object sender, ServiceEventArgs e)
        {
            Completed(sender, e);
        }

大家注意到我们是通过ServiceInvoker来调用服务的

实例化ServiceInvoker类之后就注册了ServiceEventHandler事件

此事件是服务调用完成后触发的事件(silverlight 原生的ria service也有一个completed事件)

该事件相关代码如下

    public class ServiceEventArgs : EventArgs
    {
        //服务方法的返回值
        public object Result { get; set; }
    }
    public delegate void ServiceEventHandler(object sender, ServiceEventArgs e);

在si.PrepareInvoke把需要调用的服务类名,方法名,返回值类型(如果有参数,这里还会自动加入参数)

PrepareInvoke方法如下

        public void PrepareInvoke(string ClassName,string MethodName,Type ResultType,params object[] objs)
        {
            className = ClassName;
            methodName = MethodName;
            resultType = ResultType;
            MemoryStream ms = new MemoryStream();
            var sb = new StringBuilder();            
            for(int i=0;i<objs.Length;i++)
            {
                var jsonSerializer = new DataContractJsonSerializer(objs[i].GetType());
                jsonSerializer.WriteObject(ms, objs[i]);
                var objStr = Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length);
                ms.Position = 0;
                sb.AppendFormat("p{0}=", i);
                sb.AppendFormat("{0}", objStr);
                sb.Append("&");
            }
            ms.Close();
            paramStr = sb.ToString();
        }

在此方法中主要是记录下这些信息,

另外把服务需要传入的参数序列化成JSON字符串

紧接着就调用InvokeService方法

代码如下

        public void InvokeService()
        {
            Uri serviceUri = new Uri("http://localhost/RTMDemo.Host/RTMDemo.Host.WCF.MenuService");
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(serviceUri);            
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            var requestResult = request.BeginGetRequestStream(new AsyncCallback(RequestReady), request);
            return;
        }

明眼人一看就明了了

其实就是使用HTTPWebRequest来调用服务(服务端我们托管了HttpHandler的请求)

RequestReady事件如下:

        void RequestReady(IAsyncResult asyncResult)
        {
            HttpWebRequest request = asyncResult.AsyncState as HttpWebRequest;
            Stream stream = request.EndGetRequestStream(asyncResult);
            Deployment.Current.Dispatcher.BeginInvoke(delegate()
            {
                StreamWriter writer = new StreamWriter(stream);
                writer.Write(paramStr);
                writer.Write("MethodKey=RTMDemo.Host.WCF.{0}.{1}&", className,methodName);
                writer.Flush();
                writer.Close();
                request.BeginGetResponse(new AsyncCallback(ResponseReady), request);
            });
        }

在此事件中我们把服务类名方法名和参数写入了请求流

ResponseReady事件如下

        void ResponseReady(IAsyncResult asyncResult)
        {
            HttpWebRequest request = asyncResult.AsyncState as HttpWebRequest;
            HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);
            Deployment.Current.Dispatcher.BeginInvoke(delegate()
            {
                Stream responseStream = response.GetResponseStream();
                if (resultType == null)
                {
                    Completed(this, null);
                    return;
                }
                try
                {
                    DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(resultType);
                    result = jsonSerializer.ReadObject(responseStream);
                }
                catch
                {
                }
                var se = new ServiceEventArgs();
                se.Result = result;
                Completed(this, se);
            });
        }

这个事件把服务端返回的结果(JSON数据)反序列化成实体类型,并赋值给ServiceEventArgs

然后触发了Completed事件

也就是触发我们服务端代理类的si_Completed事件

至此,调用服务端的类就解释完了

下面我们看看是怎么调用服务端的

        private void InitMenu()
        {
            var ms = new MenuService();
            ms.Completed += new ServiceEventHandler((o, re) =>
            {                
                var AllMenu = re.Result as List;
                Common.ViewUtility.AllMenu = AllMenu;
                InitTopMenu();
            });
            ms.GetAllMenu();
        }

看看是不是与ria service调用的方法有点像呢?

六.自定义系统菜单(使用自己的DataForm)

首先我们设计的窗体如下

xaml代码如下:

<location:BasePage x:Class="RTMDemo.Frame.Pages.Sys.MenuLE" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     mc:Ignorable="d"
     xmlns:dataForm="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.DataForm.Toolkit"
     xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
        xmlns:toolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit"
     xmlns:location="clr-namespace:RTMDemo.Frame.Pages"
     xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
     d:DesignWidth="640" d:DesignHeight="580" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
   <Grid x:Name="LayoutRoot" Loaded="LayoutRoot_Loaded">
        <Grid.ColumnDefinitions>
            <ColumnDefinition x:Name="CDL" Width="200"/>
            <ColumnDefinition Width="5"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <controls:GridSplitter Grid.Column="1"  HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
        <ScrollViewer Grid.Row="0" Grid.Column="0" 
                      Width="{Binding ElementName=CDL,Path=Width}" 
                      HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
            <sdk:TreeView BorderThickness="0" Name="MenuTV" SelectedItemChanged="MenuTV_SelectedItemChanged">
            </sdk:TreeView>
        </ScrollViewer>
        <Grid x:Name="MenuFormG" Grid.Column="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="60"></ColumnDefinition>
                <ColumnDefinition Width="8"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="32"></RowDefinition>
                <RowDefinition Height="32"></RowDefinition>
                <RowDefinition Height="32"></RowDefinition>
                <RowDefinition Height="32"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
                <RowDefinition Height="32"></RowDefinition>
            </Grid.RowDefinitions>
            <sdk:Label Target="{Binding ElementName=MenuNameTB}"
                       VerticalAlignment="Center" HorizontalAlignment="Right"
                       ></sdk:Label>
            <TextBox Name="MenuNameTB" Grid.Column="2" Grid.Row="0" HorizontalAlignment="Left" Width="300" 
                            VerticalAlignment="Center" Height="22" Text="{Binding MenuName, Mode=TwoWay                
                    ,NotifyOnValidationError=True,ValidatesOnExceptions=True}">
            </TextBox>

            <sdk:Label Grid.Row="1" Target="{Binding ElementName=MenuOrderTB}"
                       VerticalAlignment="Center" HorizontalAlignment="Right"
                       ></sdk:Label>
            <TextBox Name="MenuOrderTB" Grid.Column="2" Grid.Row="1" 
                     HorizontalAlignment="Left" Width="100" 
                        VerticalAlignment="Center" Height="22"     
                        Text="{Binding OrderNum,Mode=TwoWay
                    ,NotifyOnValidationError=True,ValidatesOnExceptions=True}"></TextBox>


            <sdk:Label Grid.Row="2" Content="菜单路径"
                       VerticalAlignment="Center" HorizontalAlignment="Right"
                       ></sdk:Label>
            <ComboBox x:Name="MenuUrlCB" Height="22" Width="300"
                      SelectedValue="{Binding Url,Mode=TwoWay}"
                    Grid.Column="2" Grid.Row="2" HorizontalAlignment="Left"></ComboBox>

            <TextBlock  Grid.Row="3" Text="父级菜单" VerticalAlignment="Center" HorizontalAlignment="Right"></TextBlock>
            <ComboBox Grid.Column="2" Grid.Row="3" DisplayMemberPath="MenuName" x:Name="TMenuCB"
                      Height="22" Width="100"
                    HorizontalAlignment="Left">
            </ComboBox>

            <sdk:Label Grid.Row="4" Target="{Binding ElementName=HelpTB}"
                       VerticalAlignment="Center" HorizontalAlignment="Right"
                       ></sdk:Label>
            <TextBox Grid.Column="2" Grid.Row="4" x:Name="HelpTB"
                            AcceptsReturn="True"
                            TextWrapping="Wrap"
                            VerticalScrollBarVisibility="Auto"
                        Text="{Binding MenuDes,Mode=TwoWay
                    ,NotifyOnValidationError=True,ValidatesOnExceptions=True}"></TextBox>

            <StackPanel Grid.Column="2" Grid.Row="111"  Orientation="Horizontal">
                <Button x:Name="AddBTN" Width="100" Height="22"  Margin="0 0 10 0" Content="增加"
 Click="AddBTN_Click"></Button>
                <Button x:Name="EditBTN" Width="100" Height="22" Margin="0 0 10 0"  Content="修改"
 Click="EditBTN_Click"></Button>
                <Button x:Name="DelBTN" Width="100" Height="22" Margin="0 0 10 0"  Content="删除" 
Click="DelBTN_Click"></Button>
            </StackPanel>
        </Grid>
    </Grid>
</location:BasePage>

需要说明的:

1.

所有的业务窗体都继承自BasePage类

这也是为什么xaml代码的开始处是<location:BasePage….

2.

由于左侧的树控件和右侧的Grid控件中间

有个GridSplitter控件

所以可以自由的拖动GridSplitter控件以变化左右两侧控件的大小

树控件我们暂且不提(没有什么特殊的地方)

-------------------------

在加载页面的Loaded事件中执行了如下代码

        private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
        {            
            if (IsLoaded)
            {
                return;
            }
            InitMenuTree();
            InitTypeCB();
        }

其中IsLoaded属性是基类BasePage的属性

代码如下

        protected bool IsLoaded = false;
        public BasePage()
        {
            this.Loaded += new RoutedEventHandler(BasePage_Loaded);
        }
        void BasePage_Loaded(object sender, RoutedEventArgs e)
        {
            IsLoaded = true;
        }

这样做就是为了避免重复执行InitMenuTree和InitTypeCB两个方法的代码

(tab页面切换会触发Loaded事件)

------------------------------------

先来看InitMenuTree的代码

        void InitMenuTree()
        {
            var tMenu = Common.ViewUtility.AllMenu
                    .Where(m => m.ParentId == Guid.Empty)
                    .OrderBy(m=>m.OrderNum);
            InitParentMenu(tMenu);
            foreach (var tm in tMenu)
            {
                var ttvi = new TreeViewItem();
                ttvi.Header = tm.MenuName;
                ttvi.DataContext = tm;
                if (MenuTV.Items.Count < 1)
                {
                    MenuFormG.DataContext = tm;
                    ttvi.IsSelected = true;
                }
                ttvi.IsExpanded = true;
                MenuTV.Items.Add(ttvi);
                var sMenu = Common.ViewUtility.AllMenu
                        .Where(m => m.ParentId == tm.Id)
                        .OrderBy(m => m.OrderNum);
                foreach (var sm in sMenu)
                {
                    var stvi = new TreeViewItem();
                    stvi.Header = sm.MenuName;
                    stvi.DataContext = sm;
                    ttvi.Items.Add(stvi);                    
                }
            }
        }

笔者并没有使用数据绑定的形式给控件赋值

而是直接创建了树控件的子控件来赋值的(这与我们的数据结构有关,这样做更简便一些)

MenuM类型并不是一个自引用的类型(没有记录ParentMenu只记录了ParentId)

其中InitParentMenu是初始化下拉框的函数(修改子菜单的父级菜单时用到,这里就不多说了)

        /// <summary>
        /// 构造父级菜单的combo box
        /// </summary>
        /// <param name="tMenu"></param>
        void InitParentMenu(IEnumerable<MenuM> tMenu)
        {
            var rs = tMenu.ToList();
            var TM = new MenuM();
            TM.MenuName = "请选择";
            TM.Id = Guid.Empty;
            rs.Insert(0, TM);
            TMenuCB.ItemsSource = rs;
            TMenuCB.SelectedIndex = 0;
        }

-----------------------------------------------

InitTypeCB是构造可以使用的菜单路径(下拉框)的函数

        void InitTypeCB()
        {
            var tys = Application.Current.GetType().Assembly.GetTypes().ToList();
            var results = tys.Where(m =>  m.IsPublic 
                                            && m.FullName.StartsWith("RTMDemo.Frame.Pages")
                                            && !m.FullName.EndsWith(".BasePage"))
                             .Select(m=>m.FullName.TrimStart("RTMDemo.Frame.".ToArray()))
                             .ToList();
            results.Insert(0,"请选择");
            MenuUrlCB.ItemsSource = results;
            MenuUrlCB.UpdateLayout();
            MenuUrlCB.SelectedIndex = 0;
        }

此函数反射出了所有业务窗体的类名,并赋值给了一个ComboBox,以供选择

---------------------------------------------------------------

当选中菜单树中的某一项时执行如下事件

        private void MenuTV_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            var item = MenuTV.SelectedItem as TreeViewItem;
            var menuObj = item.DataContext as MenuM;
            var fobj = Common.Utility.DeepCopy(menuObj);
            MenuFormG.DataContext = fobj;
            var parent = Common.ViewUtility.AllMenu
                       .Where(m => m.Id == menuObj.ParentId)
                       .FirstOrDefault();
            TMenuCB.SelectedItem = (parent == null ? TMenuCB.Items.FirstOrDefault() : parent);
            MenuUrlCB.SelectedItem = (string.IsNullOrEmpty(menuObj.Url) ? "请选择" : menuObj.Url);
        }

因为MenuFormG内的数据绑定元素基本上都是使用的双向绑定(更改会直接反应在实体上)

所以我们深拷贝了一个实体提供给表单(这样就不会影响现有实体的数据)

技巧:深拷贝其实就是执行了一次序列化和反序列化的过程

代码如下:

        public static object DeepCopy(object tar)
        {
            MemoryStream ms = new MemoryStream();
            var jsonSerializer = new DataContractJsonSerializer(tar.GetType());
            jsonSerializer.WriteObject(ms, tar);
            var result = jsonSerializer.ReadObject(ms);
            return result;
        }

------------------------------------------------------

下面我们来看一下增加一个菜单的方法

        private void AddBTN_Click(object sender, RoutedEventArgs e)
        {
            var obj = MenuFormG.DataContext as MenuM;
            if (FormHasError(MenuFormG))
            {
                Common.ViewUtility.Alert("数据有误不能提交");
                return;
            }
            var ms = new MenuService();
            obj.Id = Guid.NewGuid();
            obj.ParentId = (TMenuCB.SelectedItem as MenuM).Id;
            ms.Completed += new ServiceEventHandler((o, se) =>
            {
                Common.ViewUtility.Alert("增加成功");
                Common.ViewUtility.AllMenu.Add(obj);
                Reload();
            });
            ms.AddMenu(obj);
            
        }

验证客户端输入的数据是否正确的方法,是基类提供的

       protected bool FormHasError(DependencyObject form)
        {
            var items = form.GetVisuals();
            foreach (var formItem in items)
            {
                if (Validation.GetHasError(formItem))
                {
                    ((Control)formItem).Focus();
                    return true;
                }
            }
            return false;
        }
         public static IEnumerable<DependencyObject> GetVisuals(this DependencyObject root)
        {
            int count = VisualTreeHelper.GetChildrenCount(root);
            for (int i = 0; i < count; i++)
            {
                var child = VisualTreeHelper.GetChild(root, i);
                yield return child;
                foreach (var descendants in child.GetVisuals())
                {
                    yield return descendants;
                }
            }
        }

如果某一个菜单项含有错误信息,那么将验证不通过。

Reload方法也是基类提供的

        protected void Reload()
        {
            var t = this.GetType();
            var ti = this.Parent as TabItem;
            var menuObj = ti.DataContext as MenuM;
            var tc = ti.Parent as TabControl;
            tc.Items.Remove(ti);

            var obj = Activator.CreateInstance(t);
            ti = new Controls.PageContainer();
            ti.DataContext = menuObj;
            ti.Header = menuObj.MenuName;
            ti.Content = obj;
            tc.Items.Add(ti);
            tc.SelectedItem = ti;
        }

此函数也结合前面的章节来看。

--------------------------------------

至此本系列全部写完了!

源码下载


 
分享到
 
 
     


使用decj简化Web前端开发
Web开发框架形成之旅
更有效率的使用Visual Studio
MVP+WCF+三层结构搭建框架
ASP.NET运行机制浅析【图解】
编写更好的C#代码
10个Visual Studio开发调试技巧
更多...   


.NET框架与分布式应用架构设计
.NET & WPF & WCF应用开发
UML&.Net架构设计
COM组件开发
.Net应用开发
InstallShield


日照港 .NET Framework & WCF应用开发
神华信息 .NET单元测试
北京 .Net应用软件系统架构
台达电子 .NET程序设计与开发
赛门铁克 C#与.NET架构设计
广东核电 .Net应用系统架构
更多...