nopcommerce中文网

nopcommerce是国外asp.net领域一个高质量的b2c开源项目,基于EntityFramework和MVC开发,QQ群1:75272942(2000人超级群,已满) QQ群2:640322459

导航 - 搜索

.NET编程之事件总线(Event Bus)知多少?

1. 引言

事件总线这个概念对你来说可能很陌生,但提到观察者(发布-订阅)模式,你也许就很熟悉。事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。

我们来看看事件总线的处理流程:

了解了事件总线的基本概念和处理流程,下面我们就来分析下如何去实现事件总线。

2.回归本质

在动手实现事件总线之前,我们还是要追本溯源,探索一下事件的本质和发布订阅模式的实现机制。

2.1.事件的本质

我们先来探讨一下事件的概念。都是读过书的,应该都还记得记叙文的六要素:时间、地点、人物、事件(起因、经过、结果)。

我们拿注册的案例,来解释一下。
用户输入用户名、邮箱、密码后,点击注册,输入无误校验通过后,注册成功并发送邮件给用户,要求用户进行邮箱验证激活。

这里面就涉及了两个主要事件:

  1. 注册事件:起因是用户点击了注册按钮,经过是输入校验,结果是是否注册成功。
  2. 发送邮件事件:起因是用户使用邮箱注册成功需要验证邮箱,经过是邮件发送,结果是邮件是否发送成功。

其实这六要素也适用于我们程序中事件的处理过程。开发过WinForm程序的都知道,我们在做UI设计的时候,从工具箱拖入一个注册按钮(btnRegister),双击它,VS就会自动帮我们生成如下代码:

void btnRegister_Click(object sender, EventArgs e)
{
 // 事件的处理
}

其中object sender指代发出事件的对象,这里也就是button对象;EventArgs e 事件参数,可以理解为对事件的描述 ,它们可以统称为事件源。其中的代码逻辑,就是对事件的处理。我们可以统称为事件处理

说了这么多,无非是想透过现象看本质:事件是由事件源和事件处理组成

2.2. 发布订阅模式

定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。 ——发布订阅模式

发布订阅模式主要有两个角色:

  • 发布方(Publisher):也称为被观察者,当状态改变时负责通知所有订阅者。
  • 订阅方(Subscriber):也称为观察者,订阅事件并对接收到的事件进行处理。

发布订阅模式有两种实现方式:

  • 简单的实现方式:由Publisher维护一个订阅者列表,当状态改变时循环遍历列表通知订阅者。
  • 委托的实现方式:由Publisher定义事件委托,Subscriber实现委托。

总的来说,发布订阅模式中有两个关键字,通知和更新。
被观察者状态改变通知观察者做出相应更新。
解决的是当对象改变时需要通知其他对象做出相应改变的问题。

如果画一个图来表示这个流程的画,图形应该是这样的:

3 实现发布订阅模式

相信通过上面的解释,对事件和发布订阅模式有了一个大概的印象。都说理论要与实践相结合,所以我们还是动动手指敲敲代码比较好。
我将以『观察者模式』来钓鱼这个例子为基础,通过重构的方式来完善一个更加通用的发布订阅模式。
先上代码:

/// <summary>
/// 鱼的品类枚举
/// </summary>
public enum FishType
{
    鲫鱼,
    鲤鱼,
    黑鱼,
    青鱼,
    草鱼,
    鲈鱼
}

钓鱼竿的实现:

 /// <summary>
 ///     鱼竿(被观察者)
 /// </summary>
 public class FishingRod
 {
     public delegate void FishingHandler(FishType type); //声明委托
     public event FishingHandler FishingEvent; //声明事件

     public void ThrowHook(FishingMan man)
     {
         Console.WriteLine("开始下钩!");

         //用随机数模拟鱼咬钩,若随机数为偶数,则为鱼咬钩
         if (new Random().Next() % 2 == 0)
         {
             var type = (FishType) new Random().Next(0, 5);
             Console.WriteLine("铃铛:叮叮叮,鱼儿咬钩了");
             if (FishingEvent != null)
                 FishingEvent(type);
         }
     }
 }

垂钓者:

/// <summary>
///     垂钓者(观察者)
/// </summary>
public class FishingMan
{
    public FishingMan(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
    public int FishCount { get; set; }

    /// <summary>
    /// 垂钓者自然要有鱼竿啊
    /// </summary>
    public FishingRod FishingRod { get; set; }

    public void Fishing()
    {
        this.FishingRod.ThrowHook(this);
    }

    public void Update(FishType type)
    {
        FishCount++;
        Console.WriteLine("{0}:钓到一条[{2}],已经钓到{1}条鱼了!", Name, FishCount, type);
    }
}

场景类也很简单:

//1、初始化鱼竿
var fishingRod = new FishingRod();

//2、声明垂钓者
var jeff = new FishingMan("圣杰");

//3.分配鱼竿
jeff.FishingRod = fishingRod;

//4、注册观察者
fishingRod.FishingEvent += jeff.Update;

//5、循环钓鱼
while (jeff.FishCount < 5)
{
    jeff.Fishing();
    Console.WriteLine("-------------------");
    //睡眠5s
    Thread.Sleep(5000);
}

代码很简单,相信你一看就明白。但很显然这个代码实现仅适用于当前这个钓鱼场景,假如有其他场景也想使用这个模式,我们还需要重新定义委托,重新定义事件处理,岂不很累。本着”Don't repeat yourself“的原则,我们要对其进行重构。

结合我们对事件本质的探讨,事件是由事件源和事件处理组成。针对我们上面的案例来说,public delegate void FishingHandler(FishType type);这句代码就已经说明了事件源和事件处理。事件源就是FishType type,事件处理自然是注册到FishingHandler上面的委托实例。
问题找到了,很显然是我们的事件源和事件处理不够抽象,所以不能通用,下面咱们就来动手改造。

3.1. 提取事件源

事件源应该至少包含事件发生的时间和触发事件的对象。
我们提取IEventData接口来封装事件源:

/// <summary>
/// 定义事件源接口,所有的事件源都要实现该接口
/// </summary>
public interface IEventData
{
    /// <summary>
    /// 事件发生的时间
    /// </summary>
    DateTime EventTime { get; set; }

    /// <summary>
    /// 触发事件的对象
    /// </summary>
    object EventSource { get; set; }
}

自然我们应该给一个默认的实现EventData

/// <summary>
/// 事件源:描述事件信息,用于参数传递
/// </summary>
public class EventData : IEventData
{
    /// <summary>
    /// 事件发生的时间
    /// </summary>
    public DateTime EventTime { get; set; }

    /// <summary>
    /// 触发事件的对象
    /// </summary>
    public Object EventSource { get; set; }

    public EventData()
    {
        EventTime = DateTime.Now;
    }
}

针对Demo,扩展事件源如下:

public class FishingEventData : EventData
{
    public FishType FishType { get; set; }
    public FishingMan FisingMan { get; set; }
}

完成后,我们就可以去把在FishingRod声明的委托参数类型改为FishingEventData类型了,即public delegate void FishingHandler(FishingEventData eventData); //声明委托
然后修改FishingManUpdate方法按委托定义的参数类型修改即可,代码我就不放了,大家自行脑补。

到这一步我们就统一了事件源的定义方式。

3.2.提取事件处理器

事件源统一了,那事件处理也得加以限制。比如如果随意命名事件处理方法名,那在进行事件注册的时候还要去按照委托定义的参数类型去匹配,岂不麻烦。

我们提取一个IEventHandler接口:

 /// <summary>
 /// 定义事件处理器公共接口,所有的事件处理都要实现该接口
 /// </summary>
 public interface IEventHandler
 {
 }

事件处理要与事件源进行绑定,所以我们再来定义一个泛型接口:

 /// <summary>
 /// 泛型事件处理器接口
 /// </summary>
 /// <typeparam name="TEventData"></typeparam>
 public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
 {
     /// <summary>
     /// 事件处理器实现该方法来处理事件
     /// </summary>
     /// <param name="eventData"></param>
     void HandleEvent(TEventData eventData);
 }

你可能会纳闷,为什么先定义了一个空接口?这里就留给自己思考吧。

至此我们就完成了事件处理的抽象。我们再继续去改造我们的Demo。我们让FishingMan实现IEventHandler接口,然后修改场景类中将fishingRod.FishingEvent += jeff.Update;改为fishingRod.FishingEvent += jeff.HandleEvent;即可。代码改动很简单,同样在此略去。

至此你可能觉得我们完成了对Demo的改造。但事实上呢,我们还要弄清一个问题——如果这个FishingMan订阅的有其他的事件,我们该如何处理?
聪颖如你,你立马想到了可以通过事件源来进行区分处理

public class FishingMan : IEventHandler<IEventData>
{
    //省略其他代码
    public void HandleEvent(IEventData eventData)
    {
        if (eventData is FishingEventData)
        {
            //do something
        }

        if(eventData is XxxEventData)
        {
            //do something else
        }
    }
}

至此,这个模式实现到这个地步基本已经可以通用了。

4. 实现事件总线

通用的发布订阅模式不是我们的目的,我们的目的是一个集中式的事件处理机制,且各个模块之间相互不产生依赖。那我们如何做到呢?同样我们还是一步一步的进行分析改造。

4.1.分析问题

思考一下,每次为了实现这个模式,都要完成以下三步:

  1. 事件发布方定义事件委托
  2. 事件订阅方定义事件处理逻辑
  3. 显示的订阅事件

虽然只有三步,但这三步已经很繁琐了。而且事件发布方和事件订阅方还存在着依赖(体现在订阅者要显示的进行事件的注册和注销上)。而且当事件过多时,直接在订阅者中实现IEventHandler接口处理多个事件逻辑显然不太合适,违法单一职责原则。这里就暴露了三个问题:

  1. 如何精简步骤?
  2. 如何解除发布方与订阅方的依赖?
  3. 如何避免在订阅者中同时处理多个事件逻辑?

带着问题思考,我们就会更接近真相。

想要精简步骤,那我们需要寻找共性。共性就是事件的本质,也就是我们针对事件源和事件处理提取出来的两个接口。

想要解除依赖,那就要在发布方和订阅方之间添加一个中介。

想要避免订阅者同时处理过多事件逻辑,那我们就把事件逻辑的处理提取到订阅者外部。

思路有了,下面我们就来实施吧。

4.2.解决问题

本着先易后难的思想,我们下面就来解决以上问题。

4.2.1. 实现IEventHandler

我们先解决上面的第三个问题:如何避免在订阅者中同时处理多个事件逻辑?

自然是针对不同的事件源IEventData实现不同的IEventHandler。改造后的钓鱼事件处理逻辑如下:

/// <summary>
/// 钓鱼事件处理
/// </summary>
public class FishingEventHandler : IEventHandler<FishingEventData>
{
    public void HandleEvent(FishingEventData eventData)
    {
        eventData.FishingMan.FishCount++;

        Console.WriteLine("{0}:钓到一条[{2}],已经钓到{1}条鱼了!",
            eventData.FishingMan.Name, eventData.FishingMan.FishCount, eventData.FishType);

    }
}

这时我们就可以移除在FishingMan中实现的IEventHandler接口了。
然后将事件注册改为fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;即可。

4.2.2. 统一注册事件

上一个问题的解决,有助于我们解决第一个问题:如何精简流程?
为什么呢,因为我们是根据事件源定义相应的事件处理的。也就是我们之前说的可以根据事件源来区分事件。
然后呢?反射,我们可以通过反射来进行事件的统一注册。
FishingRod的构造函数中使用反射,统一注册实现了IEventHandler<FishingEventData>类型的实例方法HandleEvent

public FishingRod()
{
    Assembly assembly = Assembly.GetExecutingAssembly();

    foreach (var type in assembly.GetTypes())
    {
        if (typeof(IEventHandler).IsAssignableFrom(type))//判断当前类型是否实现了IEventHandler接口
        {
            Type handlerInterface = type.GetInterface("IEventHandler`1");//获取该类实现的泛型接口
            Type eventDataType = handlerInterface.GetGenericArguments()[0]; // 获取泛型接口指定的参数类型

            //如果参数类型是FishingEventData,则说明事件源匹配
            if (eventDataType.Equals(typeof(FishingEventData)))
            {
                //创建实例
                var handler = Activator.CreateInstance(type) as IEventHandler<FishingEventData>;
                //注册事件
                FishingEvent += handler.HandleEvent;
            }
        }
    }
}

这样,我们就可以移出场景类中的显示注册代码fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;

4.2.3. 解除依赖

如何解除依赖呢?其实答案就在本文的两张图上,仔细对比我们可以很直观的看到,Event Bus就相当于一个介于Publisher和Subscriber中间的桥梁。它隔离了Publlisher和Subscriber之间的直接依赖,接管了所有事件的发布和订阅逻辑,并负责事件的中转。

Event Bus终于要粉墨登场了!!!
分析一下,如果EventBus要接管所有事件的发布和订阅,那它则需要有一个容器来记录事件源和事件处理。那又如何触发呢?有了事件源,我们就自然能找到绑定的事件处理逻辑,通过反射触发。代码如下:

/// <summary>
/// 事件总线
/// </summary>
public class EventBus
{
    public static EventBus Default => new EventBus();

    /// <summary>
    /// 定义线程安全集合
    /// </summary>
    private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping;

    public EventBus()
    {
        _eventAndHandlerMapping = new ConcurrentDictionary<Type, List<Type>>();
        MapEventToHandler();
    }

    /// <summary>
    ///通过反射,将事件源与事件处理绑定
    /// </summary>
    private void MapEventToHandler()
    {
        Assembly assembly = Assembly.GetEntryAssembly();
        foreach (var type in assembly.GetTypes())
        {
            if (typeof(IEventHandler).IsAssignableFrom(type))//判断当前类型是否实现了IEventHandler接口
            {
                Type handlerInterface = type.GetInterface("IEventHandler`1");//获取该类实现的泛型接口
                if (handlerInterface != null)
                {
                    Type eventDataType = handlerInterface.GetGenericArguments()[0]; // 获取泛型接口指定的参数类型

                    if (_eventAndHandlerMapping.ContainsKey(eventDataType))
                    {
                        List<Type> handlerTypes = _eventAndHandlerMapping[eventDataType];
                        handlerTypes.Add(type);
                        _eventAndHandlerMapping[eventDataType] = handlerTypes;
                    }
                    else
                    {
                        var handlerTypes = new List<Type> { type };
                        _eventAndHandlerMapping[eventDataType] = handlerTypes;
                    }
                }
            }
        }
    }

    /// <summary>
    /// 手动绑定事件源与事件处理
    /// </summary>
    /// <typeparam name="TEventData"></typeparam>
    /// <param name="eventHandler"></param>
    public void Register<TEventData>(Type eventHandler)
    {
        List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)];
        if (!handlerTypes.Contains(eventHandler))
        {
            handlerTypes.Add(eventHandler);
            _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes;
        }
    }

    /// <summary>
    /// 手动解除事件源与事件处理的绑定
    /// </summary>
    /// <typeparam name="TEventData"></typeparam>
    /// <param name="eventHandler"></param>
    public void UnRegister<TEventData>(Type eventHandler)
    {
        List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)];
        if (handlerTypes.Contains(eventHandler))
        {
            handlerTypes.Remove(eventHandler);
            _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes;
        }
    }

    /// <summary>
    /// 根据事件源触发绑定的事件处理
    /// </summary>
    /// <typeparam name="TEventData"></typeparam>
    /// <param name="eventData"></param>
    public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData
    {
        List<Type> handlers = _eventAndHandlerMapping[eventData.GetType()];

        if (handlers != null && handlers.Count > 0)
        {
            foreach (var handler in handlers)
            {
                MethodInfo methodInfo = handler.GetMethod("HandleEvent");
                if (methodInfo != null)
                {
                    object obj = Activator.CreateInstance(handler);
                    methodInfo.Invoke(obj, new object[] { eventData });
                }
            }
        }
    }
}

事件总线主要定义三个方法,注册、取消注册、事件触发。还有一点就是我们在构造函数中通过反射去进行事件源和事件处理的绑定。
代码注释已经很清楚了,这里就不过多解释了。

下面我们就来修改Demo,修改FishingRod的事件触发:

/// <summary>
/// 下钩
/// </summary>
public void ThrowHook(FishingMan man)
{
    Console.WriteLine("开始下钩!");

    //用随机数模拟鱼咬钩,若随机数为偶数,则为鱼咬钩
    if (new Random().Next() % 2 == 0)
    {
        var a = new Random(10).Next();
        var type = (FishType)new Random().Next(0, 5);
        Console.WriteLine("铃铛:叮叮叮,鱼儿咬钩了");
        if (FishingEvent != null)
        {
            var eventData = new FishingEventData() { FishType = type, FishingMan = man };
            //FishingEvent(eventData);//不再需要通过事件委托触发
            EventBus.Default.Trigger<FishingEventData>(eventData);//直接通过事件总线触发即可
        }
    }
}

至此,事件总线的雏形已经形成!

5.事件总线的总结

通过上面一步一步的分析和实践,发现事件总线也不是什么高深的概念,只要我们自己善于思考,勤于动手,也能实现自己的事件总线。
根据我们的实现,大概总结出以下几条:

  1. 事件总线维护一个事件源与事件处理的映射字典;
  2. 通过单例模式,确保事件总线的唯一入口;
  3. 利用反射完成事件源与事件处理的初始化绑定;
  4. 提供统一的事件注册、取消注册和触发接口。

最后,以上事件总线的实现只是一个雏形,还有很多潜在的问题。有兴趣的不妨思考完善一下,我也会继续更新完善,尽情期待!

转自:http://www.cnblogs.com/sheng-jie/p/6970091.html

nopcommerce3.9发布了

喜讯:nopcommerce3.9正式版发布了,点击下载

突出特色
符合PCI DSS 3.2要求。密码登录失败锁定支持(可配置),不允许客户提交以前使用过的密码(可配置),密码至少每90天更改一次(可配置)。
消息模板中支持条件。例如如果不需要航运,你可以隐藏在电子邮件中的航运地址。
折扣要求支持布尔(“and”或“or”或group)。
积分的延迟使用。店主可以指定一段时间后,积分变成可用。
允许客户申请多折扣优惠券代码。
允许商店所有者配置自定义订单号。例如,添加一些前缀或日期。
允许商店所有者管理访问每个客户角色的插件(ACL)。
允许商店所有者指定产品可用范围(将显示而不是一般的“缺货”信息)。
捆绑产品(与其他产品相关的属性)。客户可以输入捆绑项目的数量。
层价格现在支持开始和结束日期(因此删除“特殊价格”产品属性)。
跟踪库存产品变化。
允许商店所有者和供应商回复产品评论。

改进
后台进行重新设计。更人性化的按钮,没有了前面的链接。重新设计的表格。货币、重量、尺寸等的输入。
安装页面重新设计。
货币舍入规则。
添加设置是否显示“默认”的项目,是否显示在顶部菜单(网页,论坛,联系我们等)
现在我们使用模型工厂,它使开发人员定制开发更容易。
现在客户可以在提交退货请求时附加文件(扫描,附加文件等)。默认禁用。
多个XML sitemap文件的支持(如果有50000个以上的记录)。
允许商店所有者指定供应商的地址。
每个商店的新闻和博客评论可以显示每个商店。
添加新闻和博客评论的审批机制。
性能优化。增加了用户代理的加载速度,减少了内存占用。现在网站需要更少的内存。
性能优化。请求之间的缓存折扣信息。在此之前,我们为每个HTTP请求加载所有折扣。
性能优化。当某些事件被触发时,我们只从缓存中移除适当的记录,而不是全部。
性能优化。在每个HTTP请求之间缓存博客和新闻评论的数量。
简化管理区添加“基本高级”模式的切换。
性能优化。现在管理区的供应商和制造商都被缓存。
性能优化。添加了加载所有类别的存储过程。默认禁用。
在管理区域中添加了必填项的提示。
退款申请可以根据自定义号码,日期,状态进行搜索。
博客和新闻评论可以根据日期,文本,批准的状态进行搜索。
付款失败通知客户。
添加描述到每个消息模板。
移动“联系我们”电子邮件到消息模板。现在,店主可以选择一个默认的电子邮件帐户,用于发送这些电子邮件和配置BCC。
贝宝支付插件。显示所有购买项目的列表(可配置)。
"Fixed Rate Shipping" 和"Shipping by weight" 插件合并成一个。
 "Nop.Plugin.Tax.CountryStateZip" 和"Nop.Plugin.Tax.FixedRate" 插件合并成一个。
"Froogle" 插件更名为"Google Shopping"”。
如果“注册方法”设置为“电子邮件验证”,客户应该在编辑后重新验证新的电子邮件。
仅支持的标记将显示在消息模板详细信息页上。
增加了一个设置,指示订单状态应被标记为“完成”(当刚刚发货或交付)。
在管理区如果没有搜索条件,隐藏“搜索”按钮。
为退款申请添加新的消息模板。
供应商可以导入产品(Excel)。
供应商可以导出订单(Excel)。
等等

nopcommerce3.8发布了

喜讯:nopcommerce3.8正式版发布了,点击下载

该版本的主要变化是增加了店铺管理员的功能,可以更方便的管理店铺以缓解超级管理员的压力,以及重新设计的管理后台等。

突出功能和变化
重新设计了管理后台(响应式)。在产品详细信息页面提供了“基本”和“高级”两种设置(店铺管理者可以选择他想看到/编辑的产品属性)。这种方式让nopcommerce的店铺管理者用起来容易很多。
同时使用多个折扣(累计折扣)。
“颜色”规格属性类型(允许在分类页面上过滤)。
增加了新的产品属性图像块,它非常类似于属性类型“颜色块”。但允许店铺管理员上传图片,而不是选择颜色。属性值对应的图像能更清楚地告诉客户他们所选择的产品。
更好的支持“店内取货”。允许一个店铺管理员配置取货的地点。
在结账页面的“配送到同一个地址”步骤复选框(可配置默认禁用)。
让店铺的管理者自己配置RMA(退货#请求数量)。

改进
产品导出导入支持属性。
根据规范属性的搜索支持“OR”。
分类页面允许店铺管理员自定义“排序”选项。
每个店铺的产品评论(可配置)。
允许在管理后台上传网站Logo。
允许一个店铺管理员创建数据库备份。
验证码支持新版本reCAPTCHA / V2。
现在客户可以在我的帐户页面上看到我的所有产品评论。
现在一个店铺的管理员可以在前台产品、类别、制造商、供应商、主题、博客文章、新闻等页面看到“管理该页面”的链接,
允许店铺管理员对产品评论进行过滤。
论坛允许用户针对每个帖子进行投票。
重新设计了订单详细页面。
高级搜索允许根据供应商进行搜索。
产品批量编辑页面允许编辑产品名称。
当一个店铺管理员在产品详细页面并点击“保存”按钮时,如果产品数量发送变化则显示警告。
缓存相关性能优化。
产品导入的性能优化。
产品导入的时候店铺管理员可以跳过大部分的产品属性(因为他们不是必需的)。
让店铺管理员指定产品是否可以退货。
支持插件描述。
性能.为我的积分页面增加分页支持。
在活动日志中显示“IP地址”。
导入导出类别和制造商。
让店铺管理员指定“在店铺自取”选项是否可用。
让店铺管理员设置发布/未发布主题。
让店铺管理员搜索/过滤的多重身份的订单(订单、发货、付款)。
允许店铺所有者指定发送时间到活动。
申请供应商帐户。允许申请者添加描述和上传Logo
允许供应商管理其信息(我的帐户页面)。
允许一个店铺管理员通过IP地址搜索/过滤客户。
允许店铺所有者指定每个供应商的最大产品数量。
在注册过程中强制输入两次电子邮件(可配置)。
允许用户编辑收藏夹(类似于购物车中的商品)。
私人信息不应该发送给自己。
贝宝支持部分退款。
运费估算使用AJAX(避免页面刷新)。
增加了更多的demo数据。
允许根据客户角色选择活动的收件人。
管理区。允许店铺所有者通过店铺来过滤类别和制造商列表。
“新订单通知”消息模板。增加支持"%Order.OrderNoteAttachmentUrl%",用来显示一个下载附件的链接。
更新Australia post API到最新版本。
升级Canada Post使用web service。
当输入HTML代码的时候管理后台不应该抛出异常。
添加日志当主题更改时候。
添加日志当“删除订单”和“编辑订单”的时候。
删除"Authorize.NET", "Verizon SMS Provider", "Facebook shop"这几个插件。从3.80版本以后,他们将作为第三方扩展插件。
在创建一个新的客户时,客户角色“注册”被默认选中。
把多语言移动语言详细页。
管理后台避免输入重复的页面大小。
设置帐户是否可下载产品。
添加社交网络页面的消息标记。
在订单列表页面删除“根据GUID搜索”。
样本数据。指定每个样品不同的SKU。
谷歌分析插件。允许店铺管理员选择部件区域。
论坛自适应。
管理区。确保有效的电子邮件地址,如果注册的角色被选中。
确保只有超级管理员可以删除其他管理员。确保只有超级管理员可以更改其他管理员的密码。
“卸载插件”按钮的颜色应该是红色的。“安装插件“是绿色的。
性能优化。在一条SQL语句里面删除大量数据。
Redis缓存显着的性能优化。
管理区。重定义了一些按钮。
在管理后台中动态隐藏产品属性(添加产品到订单,添加属性组合)。
添加一个默认的“标准”值在“电子邮件帐户”下拉在非定位标签的邮件模板详细信息”页。
在站点地图和论坛页面添加新的区域。
用户奖励积分更名为“添加积分”,以“添加(减少)积分”的方式更清楚。
不要用“全文”搜索SKU(启用时)。我们应该使用“精确匹配”。重要:如果你已经有了全文启用,然后去行政区>一般杂项设置,禁用全文并使它。这种方式将创建一个新的索引。
允许店铺管理员添加自定义标签。
开发者。更新第三方库到最新版本。
修正所有编译警告。
很多源代码重构以及bug修复。

常见问题之 - 发布到服务器后提示找不到新添加的Controller

我在做nop二次开发的过程中发现了这样一个问题:新增一个公司模块,控制器名为CompanyController,完成所有功能后发布到服务器,一切正常��但是过了一段时间(可能是应用程序池回收)后发现新加的Controller找不到了,好像dll被还原成老版本了,纠结了一段时间后,通过在官方论坛终于找到了原因:
因为有些插件里面需要引用nop.service.dll或者nop.web.dll,并且对应的属性设置成了复制到本地(这样就有可能是一个老的dll),在应用程序池回收或者重启后会iis需要重新加载所有的dll,这时候插件里面老的dll就有可能替换bin目录里面新的dll,从而导致找不到新增的Controller。

友情提醒:凡是插件里面需要引用的nop相关的dll时候一定要在属性里面把复制本地改成false。

相关链接:
http://www.nopcommerce.com/boards/t/25552/newly-added-property-not-found.aspx
http://www.nopcommerce.com/boards/t/40464/add-picture-field-in-blog.aspx

WebApi系列~通过HttpClient来调用Web Api接口

HttpClient是一个被封装好的类,主要用于Http的通讯,它在.net,java,oc中都有被实现,当然,我只会.net,所以,只讲.net中的HttpClient去调用Web Api的方法,基于api项目的特殊性,它需要有一个完全安全的环境,所以,你的api控制器看起来有点特别,只有5个方法,而且都是标准的http方法,我觉得这种设计很不错,很清晰,而且为了实现安全性,它不支持使用传统的表单数据,取而代之的是FromBody参数,它指拿HttpRequestMessage里参数,而不是所有的Request数据,这是基于安全方面的考虑。

一 Api接口参数的标准性

Get方式,可以有多个重载,有多个参数

POST方式,只能有一个参数,并且用[FromBody]约束,如果有多个参数,需要以对象的方式进行传递

Put方式,只能有两个参数,其中一个是通过Request.QueryString方式进行传递的,作为要更新对象的主键,别一个是[FromBody]字段,也是一个字段,如果多个字段需要把它封装成对象

标准接口如图

二 调用方,参数的标准性

在客户端进行接口调用时,我们以网页端为例,看一下网页端进行ajax跨域请求的代码

Get方式

    $.ajax({
            url: "http://localhost:52824/api/register",
            type: "GET",
            success: function (data) {
                console.log("json:" + data);
            }
        });

Post方式

     $.ajax({
            url: "http://localhost:52824/api/register",
            type: "POST",
            data: { '': '1' },//这里键名称必须为空,多个参数请传对象,api端参数名必须为value
            success: function (data) {
                console.log("post:" + data);
            }
        });

三 在控制台中实现Get方式获取接口数据(只有异步实现)

      /// <summary>
        /// HttpClient实现Get请求
        /// </summary>
        static async void dooGet()
        {
            string url = "http://localhost:52824/api/register?id=1&leval=5";
            //创建HttpClient(注意传入HttpClientHandler)
            var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip };

            using (var http = new HttpClient(handler))
            {
                //await异步等待回应
                var response = await http.GetAsync(url);
                //确保HTTP成功状态值
                response.EnsureSuccessStatusCode();

                //await异步读取最后的JSON(注意此时gzip已经被自动解压缩了,因为上面的AutomaticDecompression = DecompressionMethods.GZip)
                Console.WriteLine(await response.Content.ReadAsStringAsync());
            }
        }

四 在控制台中实现Post方式提交数据(只有异步实现)

     /// <summary>
        /// HttpClient实现Post请求
        /// </summary>
        static async void dooPost()
        {
            string url = "http://localhost:52824/api/register";
            var userId = "1";
            //设置HttpClientHandler的AutomaticDecompression
            var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip };
            //创建HttpClient(注意传入HttpClientHandler)
            using (var http = new HttpClient(handler))
            {
                //使用FormUrlEncodedContent做HttpContent
                var content = new FormUrlEncodedContent(new Dictionary<string, string>()       
                {
                  {"", userId}//键名必须为空
                 });

                //await异步等待回应

                var response = await http.PostAsync(url, content);
                //确保HTTP成功状态值
                response.EnsureSuccessStatusCode();
                //await异步读取最后的JSON(注意此时gzip已经被自动解压缩了,因为上面的AutomaticDecompression = DecompressionMethods.GZip)
                Console.WriteLine(await response.Content.ReadAsStringAsync());
            }

        }

五 在控制台中实现Put方式提交数据(只有异步实现)

        /// <summary>
        /// HttpClient实现Put请求
        /// </summary>
        static async void dooPut()
        {
            string url = "http://localhost:52824/api/register?userid=" + userId;
            var userId = "1";
            //设置HttpClientHandler的AutomaticDecompression
            var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip };
            //创建HttpClient(注意传入HttpClientHandler)
            using (var http = new HttpClient(handler))
            {
                //使用FormUrlEncodedContent做HttpContent
                var content = new FormUrlEncodedContent(new Dictionary<string, string>()       
                {
                   {"", "数据"}//键名必须为空
                });

                //await异步等待回应

                var response = await http.PutAsync(url, content);
                //确保HTTP成功状态值
                response.EnsureSuccessStatusCode();
                //await异步读取最后的JSON(注意此时gzip已经被自动解压缩了,因为上面的AutomaticDecompression = DecompressionMethods.GZip)
                Console.WriteLine(await response.Content.ReadAsStringAsync());
            }

        }

OK,到这里,我们的客户端如何去调用web api就讲完了,事实上,手机端,平板端也是相关的方式去调用的,它们也都有自己的HttpClient类,大同小异!

nopcommerce未来版本规划

根据nopcommerce官方的说法,下一个版本是nopCommerce3.80,最大的亮点是后台layout的重构,运用了bootstrap框架,还有一些功能的增强以及大量bug的修复,预计发布时间为7-8月份。

ASP.NET Core已经发布,但是不会被用于3.8中,这是相当新的技术,应该给它一些时间来成熟。但最有可能在下一个版本(4.0)中使用它(跳过3.90版本直接进入asp.net core时代)。

鱼头鱼尾视频教程 - 微信扫码登录插件视频教程三


相关说明:
-->基于OAuth2.0的授权模式讲解
-->实现AspNet文件夹里面需要用到的类DictionaryExtensions.cs、JsonHelper.cs、MessagingUtilities.cs、WeiXinGraphData.cs、WeiXinOpenIdData.cs及抽象类OpenAuthenticationClient.cs的代码
-->实现Core文件夹里面需要用到的类Provider.cs、OAuthAuthenticationParameters.cs的代码及创建接口IOAuthProviderWeiXinAuthorizer.cs及实现接口的类WeiXinProviderAuthorizer.cs(此类代码暂不实现)
-->实现依赖注入类DependencyRegistrar.cs的代码

该视频由鱼头鱼尾(QQ:875755898)制作
视频下载地址:http://pan.baidu.com/s/1boZmJO7
插件下载地址:Nop.Plugin.ExternalAuth.WeiXin.zip (69.8KB)

asp.net mvc自定义JsonResult类来防止MaxJsonLength超过限制

前不久在做一个项目的时候,我用到了mvc的webapi返回了一个大数据,结果报了500错误,如下图所示:

Server Error in ‘/’ Application.

Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 

Exception Details: System.InvalidOperationException: Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.

Source Error: 


An unhandled exception was generated during the execution of the current web request.
Information regarding the origin and location of the exception can be identified using the exception stack trace below.

Stack Trace:

Version Information: Microsoft .NET Framework Version:2.0.50727.4952; ASP.NET Version:2.0.50727.4955


从上面可以看出错误的原因是对象在JavaScriptSerializer序列化的时候超过了默认的最大限制,所以我们需要一个类来重写JsonResult从而允许更大的数据。

using System;
using System.Web.Script.Serialization;
 
namespace System.Web.Mvc
{
    public class LargeJsonResult : JsonResult
    {
        const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed
to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet."
;
        public LargeJsonResult()
        {
            MaxJsonLength = 1024000;
            RecursionLimit = 100;
        }
 
        public int MaxJsonLength { get; set; }
        public int RecursionLimit { get; set; }
 
        public override void ExecuteResult( ControllerContext context )
        {
            if( context == null )
            {
                throw new ArgumentNullException( "context" );
            }
            if( JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
                String.Equals( context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase ) )
            {
                throw new InvalidOperationException( JsonRequest_GetNotAllowed );
            }
 
            HttpResponseBase response = context.HttpContext.Response;
 
            if( !String.IsNullOrEmpty( ContentType ) )
            {
                response.ContentType = ContentType;
            }
            else
            {
                response.ContentType = "application/json";
            }
            if( ContentEncoding != null )
            {
                response.ContentEncoding = ContentEncoding;
            }
            if( Data != null )
            {
                JavaScriptSerializer serializer = new JavaScriptSerializer() { MaxJsonLength = MaxJsonLength, RecursionLimit = RecursionLimit };
                response.Write( serializer.Serialize( Data ) );
            }
        }
    }
}

你可以在Action里面使用
return new LargeJsonResult(){ Data = data }  来替代 return Json(data).
当然你也可以自己控制JavaScriptSerializer的MaxJsonLength:

return new LargeJsonResult() { Data = output, MaxJsonLength = int.MaxValue };

转载自:https://brianreiter.org/2011/01/03/custom-jsonresult-class-for-asp-net-mvc-to-avoid-maxjsonlength-exceeded-exception/

分享一个模拟提交带文件的表单的方法

public string PostFormData(List<FormItem> list, string uri)
        {
            string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");

            //请求
            WebRequest req = WebRequest.Create(uri);
            req.Method = "POST";
            req.ContentType = "multipart/form-data; boundary=" + boundary;

            //组织表单数据
            StringBuilder sb = new StringBuilder();
            foreach (FormItem item in list)
            {
                switch (item.ParamType)
                {
                    case ParamType.Text:
                        sb.Append("--" + boundary);
                        sb.Append("\r\n");
                        sb.Append("Content-Disposition: form-data; name=\"" + item.Name + "\"");
                        sb.Append("\r\n\r\n");
                        sb.Append(item.Value);
                        sb.Append("\r\n");
                        break;
                    case ParamType.File:
                        sb.Append("--" + boundary);
                        sb.Append("\r\n");
                        sb.Append("Content-Disposition: form-data; name=\"" + item.Name + "\"; filename=\"" + item.Value + "\"");
                        sb.Append("\r\n");
                        sb.Append("Content-Type: application/octet-stream");
                        sb.Append("\r\n\r\n");
                        break;
                }
            }

            string head = sb.ToString();
            //post字节总长度
            long length = 0;
            byte[] form_data = Encoding.UTF8.GetBytes(head);

            //结尾
            byte[] foot_data = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n");
            List<FormItem> fileList = list.Where(f => f.ParamType == ParamType.File).ToList();
            length = form_data.Length + foot_data.Length;
            foreach (FormItem fi in fileList)
            {
                FileStream fileStream = new FileStream(fi.Value, FileMode.Open, FileAccess.Read);
                length += fileStream.Length;
                fileStream.Close();
            }

            req.ContentLength = length;
            Stream requestStream = req.GetRequestStream();

            //发送表单参数
            requestStream.Write(form_data, 0, form_data.Length);
            foreach (FormItem fd in fileList)
            {
                FileStream fileStream = new FileStream(fd.Value, FileMode.Open, FileAccess.Read);
                //文件内容
                byte[] buffer = new Byte[checked((uint)Math.Min(4096, (int)fileStream.Length))];
                int bytesRead = 0;
                while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
                    requestStream.Write(buffer, 0, bytesRead);

                //结尾
                requestStream.Write(foot_data, 0, foot_data.Length);
            }
            requestStream.Close();

            //响应
            WebResponse pos = req.GetResponse();
            StreamReader sr = new StreamReader(pos.GetResponseStream(), Encoding.UTF8);
            string html = sr.ReadToEnd().Trim();
            sr.Close();

            if (pos != null)
            {
                pos.Close();
                pos = null;
            }
            if (req != null)
            {
                req = null;
            }
            return html;
        }

        public string SaveFileFromUrl(string fileName, string url)
        {
            WebResponse response = null;
            Stream stream = null;
            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                response = request.GetResponse();
                stream = response.GetResponseStream();
                if (!response.ContentType.ToLower().StartsWith("text/"))
                {
                    SaveBinaryFile(response, fileName);
                }
                else
                {
                    StreamReader sr = new StreamReader(stream, System.Text.Encoding.UTF8);
                    return sr.ReadToEnd();
                }
            }
            catch (Exception err)
            {
                return err.ToString();
            }
            return "complete";
        }

        public bool SaveBinaryFile(WebResponse response, string fileName)
        {
            bool value = true;
            byte[] buffer = new byte[1024];
            try
            {
                if (System.IO.File.Exists(fileName))
                    System.IO.File.Delete(fileName);
                Stream outStream = System.IO.File.Create(fileName);
                Stream inStream = response.GetResponseStream();
                int l;
                do
                {
                    l = inStream.Read(buffer, 0, buffer.Length);
                    if (l > 0)
                        outStream.Write(buffer, 0, l);
                }
                while (l > 0);
                outStream.Close();
                inStream.Close();
            }
            catch
            {
                value = false;
            }
            return value;
        }

        public class FormItem
        {
            public string Name { get; set; }
            public ParamType ParamType { get; set; }
            public string Value { get; set; }
        }

        public enum ParamType
        {
            Text,
            File
        }

GitHub 上一份很受欢迎的前端代码优化指南-强烈推荐收藏

看到一份很受欢迎的前端代码指南,根据自己的理解进行了翻译,但能力有限,对一些JS代码理解不了,如有错误,望斧正。

HTML

语义化标签

HTML5 提供了很多语义化元素,更好地帮助描述内容。希望你能从这些丰富的标签库中受益。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- bad -->
<div id="main">
  <div class="article">
    <div class="header">
      <h1>Blog post</h1>
      <p>Published: <span>21st Feb, 2015</span></p>
    </div>
    <p>…</p>
  </div>
</div>
 
<!-- good -->
<main>
  <article>
    <header>
      <h1>Blog post</h1>
      <p>Published: <time datetime="2015-02-21">21st Feb, 2015</time></p>
    </header>
    <p>…</p>
  </article>
</main>

请确保正确使用语义化的标签,错误的用法甚至不如保守的用法。

1
2
3
4
5
6
7
8
9
10
11
<!-- bad -->
<h1>
  <figure>
    <img alt=Company src=logo.png>
  </figure>
</h1>
 
<!-- good -->
<h1>
  <img alt=Company src=logo.png>
</h1>

简洁

确保代码简洁性,不要再采用XHTML的旧做法。

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
<!-- bad -->
<!doctype html>
<html lang=en>
  <head>
    <meta http-equiv=Content-Type content="text/html; charset=utf-8" />
    <title>Contact</title>
    <link rel=stylesheet href=style.css type=text/css />
  </head>
  <body>
    <h1>Contact me</h1>
    <label>
      Email address:
      <input type=email placeholder=you@email.com required=required />
    </label>
    <script src=main.js type=text/javascript></script>
  </body>
</html>
 
<!-- good -->
<!doctype html>
<html lang=en>
  <meta charset=utf-8>
  <title>Contact</title>
  <link rel=stylesheet href=style.css>
 
  <h1>Contact me</h1>
  <label>
    Email address:
    <input type=email placeholder=you@email.com required>
  </label>
  <script src=main.js></script>
</html>

可用性

可用性不应该是事后才考虑的事情。你不必成为WCAG专家来改进网站,你可以通过简单的修改做出不错的效果,例如;

  • 正确使用alt属性
  • 确保链接和按钮正确使用(不要用<div class="button">这种粗暴的做法)
  • 不依赖于颜色来传达信息
  • 给表单做好lable标记
1
2
3
4
5
<!-- bad -->
 <h1><img alt="Logo" src="logo.png"></h1>
 
 <!-- good -->
 <h1><img alt="My Company, Inc." src="logo.png"></h1>

语言

定义语言和字符编码是可选项,建议在文档级别处定义。使用UTF-8编码。

1
2
3
4
5
6
7
8
9
10
<!-- bad -->
<!doctype html>
<title>Hello, world.</title>
 
<!-- good -->
<!doctype html>
<html lang=en>
  <meta charset=utf-8>
  <title>Hello, world.</title>
</html>

性能

除非有非要在加载内容前加载脚本的必要性由,不然别这样做,这样会阻碍网页渲染。如果你的样式表很大,必须独立放到一个文件里。两次HTTP 请求不会显著降低性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- bad -->
<!doctype html>
<meta charset=utf-8>
<script src=analytics.js></script>
<title>Hello, world.</title>
<p>...</p>
 
<!-- good -->
<!doctype html>
<meta charset=utf-8>
<title>Hello, world.</title>
<p>...</p>
<script src=analytics.js></script>

CSS

分号

不能漏写分号

1
2
3
4
5
6
7
8
9
/* bad */
div {
  color: red
}
 
/* good */
div {
  color: red;
}

盒模型

整个文档的盒模型应该要相同,最好使用global * { box-sizing: border-box; }定义。不要修改某个元素的盒模型。

1
2
3
4
5
6
7
8
9
10
11
/* bad */
div {
  width: 100%;
  padding: 10px;
  box-sizing: border-box;
}
 
/* good */
div {
  padding: 10px;
}

尽量不要改变元素默认行为。保持默认的文本流。比如,移出一个图片下面的一个白块,不影响原本的显示:

1
2
3
4
5
6
7
8
9
/* bad */
img {
  display: block;
}
 
/* good */
img {
  vertical-align: middle;
}

类似的,尽量不要改变浮动方式。

1
2
3
4
5
6
7
8
9
10
11
12
/* bad */
div {
  width: 100px;
  position: absolute;
  right: 0;
}
 
/* good */
div {
  width: 100px;
  margin-left: auto;
}

定位

有很多CSS定位方法,尽量避免使用以下方法,根据性能排序: