翻译 - ASP.NET Core 基本知识 - 依赖注入

翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-5.0

ASP.NET Core 支持依赖注入软件设计模式,依赖注入是一种在类和它们的依赖之间获取控制反转的技术(Inversion of Control (IoC))。

更多关于 MVC controllers 依赖注入的信息,请查看 Dependency injection into controllers in ASP.NET Core

更多关于在其它应用程序中使用依赖注入而不是 web 应用程序,请查看 Dependency injection in .NET

更多关于依赖注入选项的信息,请查看 Dependency injection in .NET

该主题提供了 ASP.NET Core 中的依赖注入的信息。关于使用依赖注入的基本文档包含在 Dependency injection in .NET 中。

依赖注入概览

依赖是另外一个对象依赖的对象。 查看下面包含一个 WriteMessage 方法的类 MyDependency,会被其他类依赖:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

一个类可能会为了使用 WriteMessage 方法去创建一个 MyDependency 对象。在下面的例子中, MyDependency 类是 IndexModel 类的一个依赖:

public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet created this message.");
    }
}

这个类创建 MyDependency 对象的时候直接依赖 MyDependency 类。像前面这个例子,代码依赖是有问题的并且由于下面的原因应该被避免:

  • 用一个不同的实现替换 MyDependency 的时候,IndexModel 类必须被修改
  • 如果 MyDependency 也存在依赖,IndexModel 也必须配置它们。在一个大的工程中,大量的类会依赖 MyDependency,配置代码在应用程序中会变的分散。
  • 这种实现很难去做单元测试。应用程序应该使用一个可模拟的或者可存根的 MyDependency 类,上面这种方法是不可能实现的。

依赖注入通过以下方法解决了这些问题:

  • 使用接口或者基类抽象依赖的实现
  • 在一个服务同期中注册依赖。ASP.NET Core 提供了一个内置的服务容器,IServiceProvider。服务通常在应用程序的 Startup.ConfigureServices 方法中注册。
  • 在使用依赖的地方,通过类的构造方法注入服务。框架负责创建依赖和在不再需要的时候释放掉依赖。

在示例应用程序中,IMyDependency 接口定义了 WriteMessage 方法:

public interface IMyDependency
{
    void WriteMessage(string message);
}

这个接口被具体的类型 MyDependency 实现:

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

示例应用程序使用具体的类型 MyDependency 注册 IMyDependency 服务。AddScoped 方法注册一声明周期为 scoped 的服务,声明周期的范围为一个请求内。服务生命周期 (Service lifetimes) 在主题的下面会描述到。

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency>();

    services.AddRazorPages();
}

在示例应用程序中,IMyDependency 服务被请求到,用来调用 WriteMessage 方法:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

控制器通过使用依赖注入(DI)模式:

  • 不使用具体的类型 MyDependency,仅仅使用到了它实现的 IMyDependency 接口。这使得不用修改控制器就可以很容易改变实现。
  • 不用创建 MyDependency 实例,它的创建通过 DI 容器。

IMyDependency 接口的实现可以通过使用内置的日志 API 提升:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

更新后的 ConfigureServices 方法住了新的 IMyDependency 的实现:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency2>();

    services.AddRazorPages();
}

MyDependency2 依赖于 ILogger<TCategoryName>,在构造方法中请求。ILogger<TCategoryName> 是框架提供的服务 framework-provided service

链式调用使用依赖注入并不罕见。每个被请求的依赖依次请求他自己的依赖。容器解析图中的依赖并返回完全解析的服务。必须解析的依赖项集合通常被称为一个依赖树,依赖图,或者对象图。

容器利用 (generic) open types 解析 ILogger<TCategoryName>,消除了每一个 (generic) constructed type 注册的必要性。

在依赖注入术语中,一个服务是指:

  • 通常指提供服务给其它对象的对象,例如 IMyDependency 服务
  • 不是指 web 服务,尽管一个服务可能使用 web 服务

框架提供了一个健壮的日志 (logging) 系统。前面示例中的 IMyDependency 实现被用来展示简单的依赖注入 (DI),不是实现日志。大多数的应用程序不需要编写自己的日志系统。下面的代码展示了使用默认的日志系统,而不需要在 ConfigureServices 中注册任何服务:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;
    
    pulic AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }

    public string Message { get; set; }

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}    

使用上面的代码不需要更新 ConfigureServices,因为 logging 是框架内部提供的。

注入到 Startup 中的服务

服务可以通过 Startup 构造方法和 Startup.Configre 方法注入。

在使用 Generic Host 的时候,只有下列的服务可以注入到 Startup 构造方法:

任何使用 DI 容器注册的服务都可以被注入到 Startup.Configure 方法:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    ...
}

更多信息请查看 App startup in ASP.NET Core and Access configuration in Startup

使用扩展方法注册服务组

ASP.NET Core 框架使用一个约定来注册一组相关的服务。约定通过一个单独的 Add{GROUP_NAME} 扩展方法注册框架特性需要的服务。例如, AddControllers 扩展方法注册 MVC 控制器需要的服务。

下面的代码通过使用 individual 用户账号的 Razor Pages 模板生成的,并且使用扩展方法 AddDbContext 和 AddDefaultIdentity 展示了如何添加额外的服务到容器中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

考虑下面带有注册服务和配置选项的 ConfigureServices 方法:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(
        Configuration.GetSection(PositionOptions.Position));
    services.Configure<ColorOptions>(
        Configuration.GetSection(ColorOptions.Color));

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddScoped<IMyDependency2, MyDependency2>();

    services.AddRazorPages();
}

相关的一组注册可以移动到一个扩展方法中去注册服务。例如,配置服务可以添加到下面的类中:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }
    }
}

其余的服务都注册到一个相似的勒种,下面的 ConfigureServices 方法使用了新的扩展方法去注册服务:

public void ConfigureServices(IServiceCollection services)
{
    services.AddConfig(Configuration)
            .AddMyDependencyGroup();

    services.AddRazorPages();
}

 备注:每一个 services.Add{GROUP_NAME} 扩展方法添加和潜在的配置服务。例如,AddControllersWithViews 添加 MVC 控制器要求带有 views,AddRazorPages 要求添加 Razor Pages 服务。我们建议应用程序遵循这样的命名约定。把扩展方法放到命名空间 Microsoft.Extensions.DependencyInjection 中封装一组服务的注册。

服务生命周期

在 Dependency injection in .NET 查看 生命周期Service lifetimes

使用下面的一种方法在中间件中使用 scoped 服务:

  • 注入服务到中间件的 Invoke 或者 InvokeAsync 方法中。使用构造方法注入 (constructor injection) 会抛出运行时异常,因为会强制 socped 服务表现为一个单例一样。Lifetime and registration options 中的示例展示了使用 InvokeAsync 的方法。
  • 使用基于工厂的中间件。通过这种方法注册的中间件在每一次客户端请求(连接)都会被激活,这允许 scoped 服务可以被注入到中间件的 InvokeAsync 方法中。

更多信息请查看 Write custom ASP.NET Core middleware

服务注册方法

在 Dependency injection in .NET 查看 生命周期 Service registration methods

当测试模拟类型的时候,通常会使用多个实现。

只使用一个实现类型注册一个服务等同于使用同样的实现和服务类型。这就是为什么服务的多个实现不能使用不带有明确服务类型的方法去注册。这些方法可以注册一个服务的多个实例,但是他们全部拥有相同的实现类型。

上面任意一个服务注册的方法都可以用来注册相同服务类型的多个服务实例。在下面这个例子中, AddSingleton 使用 IMyDependency 作为服务类型被调用两次。第二次调用 AddSingleton 解析 IMyDependency 的时候覆盖了前一个,并且服务在通过 IEnumerable<IMyDependency> 被解析的时候添加到前一个上面。服务被解析的顺序按照它们被注册顺序通过 IEnumerable<{SERVICE}> 解析。

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

构造方法注入行为

查看 Dependency injection in .NET 中的 Constructor injection behavior

Entity Framework contexts

默认的, Entity Framework contexts 使用 scoped lifetime 被添加到服务容器中,因为 web 应用程序的的数据库的操作对于客户端的请求通常是有范围的。可以指定一个生命周期通过使用一个 AddDbContext 的重载方法来使用一个不同的生命周期。指定生命周期的服务不应该使用生命周期比服务生命周期更短的 database context。

生命周期和注册选项

为了展示服务生命周期和它们注册选项的不同,考虑下面的接口,将任务表示为带有标识符的操作,OperationId。根据为下面接口如何配置一个操作服务的生命周期,当被一个类请求的时候,容器提供了相同或者不同的服务实例:

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

下面的 Operation 类实现了前面的所有接口。Operation 构造方法生成了一个 GUID 并且在 OperationId 属性中存储了最后四个字符。

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

 Startup.ConfigureServices 方法使用命名的生命周期创建了多个 Operation 类的注册:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();

    services.AddRazorPages();
}

 

示例应用程序展示了请求内和请求之间的对象的生命周期。IndexModel 和 中间件请求每一种 IOperation 类型和日志输出每一个 OperationId:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

 和 IndexModel 类似,中间件解析相同的服务:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationTransient transientOperation,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Scoped 服务必须在 InvokeAsync 方法中解析:

public async Task InvokeAsync(HttpContext context,
    IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + _transientOperation.OperationId);
    _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

日志输出了:

  • Transient 对象总是不同的。transient OperationId 在 IndexModel 和 中间件中都是不同的。
  • Scoped 对象对同一个请求都是相同的,但是不同的请求不同
  • Singleton 对象对于每一个请求都是相同的

为了减少日志输出,在 appsettings.Development.json 中设置 "Logging:LogLevel:Microsoft:Error":

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

在 Main 方法中调用服务

使用 IServiceScopeFactory.CreateScope 创建一个 IServiceScope 在应用程序范围内解析一个 scoped 服务。这种获取一个 scoped 服务对于在启动时运行初始化任务是帮助的。

下面示例展示了如何在 Program.Main 中获取 scoped IMyDependency 服务,然后调用 WriteMessage:

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var myDependency = services.GetRequiredService<IMyDependency>();
                myDependency.WriteMessage("Call services from main");
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Scope 验证

查看 Dependency injection in .NET 中的 Constructor injection behavior

更多信息查看 Scope validation

请求服务

服务通过 HttpContext.RequestServices collection 暴露,在一个 ASP.NET Core 请求内部可以使用。当服务在一个请求内部被请求,服务和它们的依赖从 RequestServices collection 中解析。

框架为每一个请求创建一个 scope,RequestServices 暴露 scoped service provider。所有的 scoped 服务只要请求是活动的,那么它就是有效的。

注意

比起从 RequestServices collection 中解析服务,把依赖作为构造方法的参数更好。这会使得类更加容易测试。

为依赖注入设计服务

当为依赖注入设计服务的时候:

  • 避免有状态的,静态的类和成员。设计应用程序应该避免创建全局状态而使用 singleton 服务代替
  • 避免在服务内部直接创建依赖类的实例。直接实例化使得代码和一个具体的实现耦合起来
  • 使得服务小,结构良好和易于测试

如果一个类有很多注入的依赖,那可能意味着这个类负有太多的责任,违背了 Single Responsibility Principle (SRP)。可以尝试通过转移一部分人去到一个新的类中来解构这个类。谨记 Razor Pages model 类和 MVC 控制器类重点应该放在 UI 界面上。

服务的释放

容器会调用 Dispose 释放他创建的 IDisposable 类型的服务。从容器解析的服务永远都不应该由开发者释放。如果一个类型或者工厂被注册为一个 singleton,容器会自动释放 singleton 服务。

下面的例子中,服务通过容器创建并自动释放:

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    
    var myKey = Configuration["MyKey"];
    services.AddSingleton<IService3>(sp => new Service3(myKey));

    services.AddRazorPages();
}
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

在每一次刷新 Index Page 后,调试控制台输出了下面的信息:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet
Service1.Dispose

不通过容器创建的服务

考虑下面的代码:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new Service1());
    services.AddSingleton(new Service2());

    services.AddRazorPages();
}

在上面的代码中:

  • 服务实例不是通过服务容器创建的
  • 框架不会自动释放创建的服务
  • 开发者负责释放创建的服务

Transient  和 共享实例的 IDisposeable 编程指南

查看 Dependency injection in .NET 中的 IDisposable guidance for Transient and shared instance

替换默认的服务容器

查看 Dependency injection in .NET 中的 Default service container replacement

建议

查看 Dependency injection in .NET 中的 Recommendations

  • 避免使用 service locator pattern。例如,当可以使用依赖注入(DI)的时候,不要调用 GetService 去获取一个服务实例:
    错误的:


    正确的:
    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }

     

  • 另外一个 service locator 变体是在运行的时候注入一个工厂解析服务。这两种做法都混合了控制反转 (Inversion of Control) 的策略。
  • 避免静态的获取 HttpContext (例如, IHttpContextAccessor.HttpContext)。
  • 避免在 ConfigureServices 中调用 BuildServiceProvider。通常在开发者想在 ConfigureServices 中解析一个服务的时候会调用 BuildServiceProvider。例如,考虑 LoginPath 在配置中被加载的情况。避免下面的方法:

前面这个图片中,显示绿色波浪线的 services.BuildServiceProvider 显示了 ASP0000 警告:

ASP0000 Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'.

调用 BuildServiceProvider 创建了第二个容器,这会导致创建分裂的单利,引起跨多个容器对象引用图。

一个正确的方式是使用依赖注入内置的 options pattern:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie();

    services.AddOptions<CookieAuthenticationOptions>(
                        CookieAuthenticationDefaults.AuthenticationScheme)
        .Configure<IMyService>((options, myService) =>
        {
            options.LoginPath = myService.GetLoginPath();
        });

    services.AddRazorPages();
}
  • 能够释放的 transient 服务被容器捕获到并释放。如果服务是从顶级容器中解析的,这可能会导致内存泄漏
  • 打开 scope validation 确保不包含 singletons 的应用程序捕获到 scoped 服务。更多信息,查看 Scope validation

与所有建议一样,你可能会遇到需要忽略建议的情况。例外是很少有的,大部分特别的情况都在框架自身。

依赖注入(DI)是获取 static / global 的方法模型。你可能不能意识到依赖注入的好处,如果你把它和静态对象获取混淆了的话。

依赖注入(DI)中多租户的推荐模式

Orchard Core 是建立在 ASP.NET Core 之上的创建模块化的,多租户应用程序的框架。更多信息查看 Orchard Core Documentation

查看 Orchard Core samples 中的示例如何使用 Orchard Core Framework 而不使用 CMS-specific 特性建立模块化和多租户的应用程序。

原文地址:https://www.cnblogs.com/huangzhengguo/p/14252746.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


数组的定义 Dim MyArray MyArray = Array(1‚5‚123‚12‚98) 可扩展数组 Dim MyArray() for i = 0 to 10
\'参数: \'code:要检测的代码 \'leixing:html或者ubb \'nopic:代码没有图片时默认值
演示效果: 代码下载: 点击下载
环境:winxp sp2 ,mysql5.0.18,mysql odbc 3.51 driver 表采用 myisam引擎。access 2003  不同的地方: 
其实说起AJAX的初级应用是非常简单的,通俗的说就是客户端(javascript)与服务端(asp或php等)脚本语言的数据交互。
<% ’判断文件名是否合法 Function isFilename(aFilename)  Dim sErrorStr,iNameLength,i  isFilename=TRUE
在调用的时候加入判断就行了. {aspcms:navlist type=0 } {if:[navlist:i]<6} < li><a href=\"[navlist:link]\" target=\"_top\">[navlist:name]</a> </li>
导航栏调用 {aspcms:navlist type=0}     <a href=\"[navlist:link]\">[navlist:name]</a>
1.引入外部文件: {aspcms:template src=infobar.html} 2.二级下拉菜单 <ul class=\"nav\">
downpic.asp页面:  <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">
Cookies是数据包,可以让网页具有记忆功能,在某台电脑上记忆一定的信息。Cookies的工作原理是,第一次由服务器端写入到客户端的系统中。以后每次访问这个网页,都是先由客户端将Cookies发送到服务器端,再由服务器端
很简单,在需要调用的地方用这种模式 {aspcms:content sort={aspcms:sortid} num=17 order=isrecommend}
网站系统使用ACCESS数据库时,查询时怎么比较日期和时间呢?为什么常常比较出来却是错误的呢?比如早的日期比迟的日期大?
str1=\"1235,12,23,34,123,21,56,74,1232\" str2=\"12\" 问题:如何判断str2是否存在str1中,要求准确找出12,不能找出str1中的1235、123、1232
实例为最新版本的kindeditor 4.1.5. 主要程序: <% Const sFileExt=\"jpg|gif|bmp|png\" Function ReplaceRemoteUrl(sHTML,sSaveFilePath,sFileExt)
用ASP实现搜索引擎的功能是一件很方便的事,可是,如何实现类似3721的智能搜索呢?比如,当在搜索条件框内输入“中国人民”时,自动从中提取“中国”、“人民”等关键字并在数据库内进行搜索。看完本文后,你就可以发
首先感谢ASPCMS官网注册用户xing0203的辛苦付出!一下为久忆YK网络转载原创作者xing0203的文章内容!为了让小白更加清楚的体验替换过程,久忆YK对原文稍作了修改!
数据库连接: <% set conn=server.createobject(\"adodb.connection\") conn.open \"driver={microsoft access driver (*.mdb)};dbq=\"&server.mappath(\"数据库名\")
第1步:修改plugins下的image/image.js 找到\'<input type=\"button\" class=\"ke-upload-button\" value=\"\' + lang.upload + \'\" />\',
asp函数: <% Const sFileExt=\"jpg|gif|bmp|png\" Function ReplaceRemoteUrl(sHTML,sSaveFilePath,sFileExt)