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)。
等等

教你一招 - nopcommerce如何启用redis缓存?

从nopcommerce3.7开始就支持redis缓存了,但是默认没有启用,启用步骤如下:
1、首先安装redis服务器(如果没有的话),参考网址 http://www.redis.net.cn/tutorial/3503.html
2、然后修改web.config如下:
<RedisCaching Enabled="true" ConnectionString="localhost:6379,allowAdmin=true" />
注意要启用allowAdmin=true,否则后台清理缓存的时候会报错。
3、刷新页面就可以了,在Redis Desktop Manager里面看下缓存数据吧。


分享是一种美。版权所有,转载请注明出处 http://www.nopchina.net/

Autofac怎么依赖注入静态方法

Autofac一般是通过Contrller的构造函数或者属性来注入,但是这有一个共同点就是调用这个类的方法一般都是实例方法,也就是要实例化这个类才能调用它的方法。但是如果它是一个静态方法我们又该怎么办呢?其实也很简单,下面我们就通过一个写日志的组件来介绍怎么实现ASP.NET MVC5类的静态方法的依赖注入。因为考虑到很多地方都要用到写日志这个方法,而且我们又不想每次调用都需要new一个对象,所以把调用方法封装成一些静态方法。

DependencyRegistrar.cs

  1. using Autofac;
  2. using Autofac.Integration.Mvc;
  3. using Lanhu.Services;
  4. using Lanhu.Services.MetionNowOrder;
  5. using Lanhu.Services.Member;
  6. using System.Web.Mvc;
  7. namespace Lanhu.Admin
  8. {
  9. public class DependencyRegistrar
  10. {
  11. public static void RegisterDependencies()
  12. {
  13. var builder = new ContainerBuilder();
  14. builder.RegisterControllers(typeof(MvcApplication).Assembly);
  15. builder.RegisterType<Log_UserLogonService>().As<ILog_UserLogon>().InstancePerLifetimeScope();
  16. builder.RegisterType<Log_UserOperateService>().As<ILog_UserOperate>().InstancePerLifetimeScope();
  17. //autofac 注册依赖
  18. IContainer container = builder.Build();
  19. DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
  20.     }
  21.  }
  22. }

在App_Start事件中调用��面的会依赖注入代码:

  1. using Lanhu.Core;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.linq;
  5. using System.Web;
  6. using System.Web.Mvc;
  7. using System.Web.Optimization;
  8. using System.Web.Routing;
  9. namespace Lanhu.Admin
  10. {
  11. public class MvcApplication : System.Web.HttpApplication
  12. {
  13. protected void Application_Start()
  14. {
  15. AreaRegistration.RegisterAllAreas();
  16. FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
  17. RouteConfig.RegisterRoutes(RouteTable.Routes);
  18. BundleConfig.RegisterBundles(BundleTable.Bundles);
  19. DependencyRegistrar.RegisterDependencies();
  20. }
  21. }
  22. }

LogFacade.cs:

  1. using Lanhu.Model;
  2. using Lanhu.Services;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using System.Web;
  9. using System.Web.Mvc;
  10. namespace Lanhu.Admin.Infrastructure
  11. {
  12. public class LogFacade
  13. {
  14. public static bool AddLogonLog(MLog_UserLogon model)
  15. {
  16. var service = DependencyResolver.Current.GetService<ILog_UserLogon>();
  17. return service.Insert(model);
  18. }
  19. public static bool AddOperateLog(string info,bool isError=false)
  20. {
  21. var service = DependencyResolver.Current.GetService<ILog_UserOperate>();
  22. if (isError)
  23. model.ErrorMsg = info;
  24. else
  25. model.Msg = info;
  26. return service.Insert(model);
  27. }
  28. public static bool AddOperateLog(MLog_UserOperate model)
  29. {
  30. var service = DependencyResolver.Current.GetService<ILog_UserOperate>();
  31. return service.Insert(model);
  32. }
  33. }
  34. }

上面封装了三个静态方法,在方法里面通过DependencyResolver.Current.GetService从Ioc容器中获取相应类型的依赖对象。这个LogFacade使用外观模式,让调用显得很简单。

后台提示消息通知插件 For Nopcommerce V3.8

后台提示消息通知插件,替换nop原生的提示消息通知插件。

效果图

下载:48425_117972_AdminNotificationPlugin.zip (539.4KB)

源代码放在\Plugins目录下,用vs打开重新生成。

转载:http://www.nopcommerce.com/p/2386/admin-notification-plugin-.aspx

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

RESTFUL SERVICE 插件 For Nopcommerce V3.7

一款resetful service插件,里面加入了简单的token认证,token通过后台配置页面进行配置,调用api方法的时候需要传递此token来和后台的进行验证,下载后通过后台插件管理安装、配置即可使用。

下载:61187_155782_Nop.Plugin.Misc.RestService.zip (59.1KB)

源代码放在\Plugins目录下,用vs打开重新生成。