博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Orchard详解--第八篇 拓展模块及引用的预处理
阅读量:5105 次
发布时间:2019-06-13

本文共 24263 字,大约阅读时间需要 80 分钟。

  从上一篇可以看出Orchard在处理拓展模块时主要有两个组件,一个是Folder另一个是Loader,前者用于搜索后者用于加载。

  其中Folder一共有三个:Module Folder、Core Folder、ThemeFolder。Loader有引用加载器(Referenced Module Loader)、核心模块加载器(Core Module Loader)、预编译模块加载器(Precompiled Module Loader)、动态模块加载器(Dynamic Module Loader)。它们在代码里可以看到,在创建容器时对与Folder和Loader的注册:

1               builder.RegisterType
().As
().SingleInstance(); 2 builder.RegisterType
().As
().SingleInstance(); 3 builder.RegisterType
().As
().SingleInstance(); 4 { 5 builder.RegisterType
().As
().SingleInstance(); 6 builder.RegisterType
().As
().SingleInstance() 7 .WithParameter(new NamedParameter("paths", extensionLocations.ModuleLocations)); 8 builder.RegisterType
().As
().SingleInstance() 9 .WithParameter(new NamedParameter("paths", extensionLocations.CoreLocations));10 builder.RegisterType
().As
().SingleInstance()11 .WithParameter(new NamedParameter("paths", extensionLocations.ThemeLocations));12 13 builder.RegisterType
().As
().SingleInstance();14 builder.RegisterType
().As
().SingleInstance();15 builder.RegisterType
().As
().SingleInstance();16 builder.RegisterType
().As
().SingleInstance();17 builder.RegisterType
().As
().SingleInstance();18 }

  这里需要注意的是,除了以上的Loader外,这里还有一个Raw Theme Extension Loader。

  接下来看Folder是如何工作的:
  三个Folder的实现可以说是一致的,都是通过ExtensionHarvester去获取一个ExtensionDescriptior列表:

  

1      public IEnumerable
AvailableExtensions() {2 return _extensionHarvester.HarvestExtensions(_paths, DefaultExtensionTypes.Module, "Module.txt", false/*isManifestOptional*/);3 }

  仅仅是路径和文件名称(Module.txt和Theme.txt)不同。所以可以这样说,模块和主题的搜索工作是通过ExtensionHarvester完成的。

1     public IEnumerable
HarvestExtensions(IEnumerable
paths, string extensionType, string manifestName, bool manifestIsOptional) { 2 return paths 3 .SelectMany(path => HarvestExtensions(path, extensionType, manifestName, manifestIsOptional)) 4 .ToList(); 5 } 6 private IEnumerable
HarvestExtensions(string path, string extensionType, string manifestName, bool manifestIsOptional) { 7 string key = string.Format("{0}-{1}-{2}", path, manifestName, extensionType); 8 return _cacheManager.Get(key, true, ctx => { 9 if (!DisableMonitoring) {10 Logger.Debug("Monitoring virtual path \"{0}\"", path);11 ctx.Monitor(_webSiteFolder.WhenPathChanges(path));12 }13 return AvailableExtensionsInFolder(path, extensionType, manifestName, manifestIsOptional).ToReadOnlyCollection();14 });15 }

  从上面代码可以看出Harvester内部还使用了缓存机制,以路径、文件名称和类型(模块还是主题)来作为关键字。它缓存的内容是当前目录下的一个拓展描述列表。

  描述信息包括:

1     public string Location { get; set; } 2         public string Id { get; set; } 3         public string VirtualPath { get { return Location + "/" + Id; } } 4         public string ExtensionType { get; set; } 5         public string Name { get; set; } 6         public string Path { get; set; } 7         public string Description { get; set; } 8         public string Version { get; set; } 9         public string OrchardVersion { get; set; }10         public string Author { get; set; }11         public string WebSite { get; set; }12         public string Tags { get; set; }13         public string AntiForgery { get; set; }14         public string Zones { get; set; }15         public string BaseTheme { get; set; }16         public string SessionState { get; set; }17         public LifecycleStatus LifecycleStatus { get; set; }18 19         public IEnumerable
Features { get; set; }
View Code

  功能描述信息包括:

1         public ExtensionDescriptor Extension { get; set; }2         public string Id { get; set; }3         public string Name { get; set; }4         public string Description { get; set; }5         public string Category { get; set; }6         public int Priority { get; set; }7         public IEnumerable
Dependencies { get; set; }
View Code

  Harvester中的GetDescriptorForExtension和GetFeaturesForExtension两个方法分别用于通过Module.txt或Theme.txt文件来获取拓展描述和功能描述信息(此处暂未对功能描述信息的依赖列表赋值)。其中拓展会作为一个默认的功能。

  Loader

  当Folder完成描述信息的构建后,Loader将通过这些描述信息来探测和加载模块。以下是Loader的接口定义:

1     public interface IExtensionLoader { 2         int Order { get; } 3         string Name { get; } 4  5         IEnumerable
ProbeReferences(ExtensionDescriptor extensionDescriptor); 6 Assembly LoadReference(DependencyReferenceDescriptor reference); 7 void ReferenceActivated(ExtensionLoadingContext context, ExtensionReferenceProbeEntry referenceEntry); 8 void ReferenceDeactivated(ExtensionLoadingContext context, ExtensionReferenceProbeEntry referenceEntry); 9 bool IsCompatibleWithModuleReferences(ExtensionDescriptor extension, IEnumerable
references);10 11 ExtensionProbeEntry Probe(ExtensionDescriptor descriptor);12 ExtensionEntry Load(ExtensionDescriptor descriptor);13 14 void ExtensionActivated(ExtensionLoadingContext ctx, ExtensionDescriptor extension);15 void ExtensionDeactivated(ExtensionLoadingContext ctx, ExtensionDescriptor extension);16 void ExtensionRemoved(ExtensionLoadingContext ctx, DependencyDescriptor dependency);17 18 void Monitor(ExtensionDescriptor extension, Action
monitor);19 20 ///
21 /// Return a list of references required to compile a component (e.g. a Razor or WebForm view)22 /// depending on the given module. 23 /// Each reference can either be an assembly name or a file to pass to the 24 /// IBuildManager.GetCompiledAssembly() method (e.g. a module .csproj project file).25 /// 26 IEnumerable
GetCompilationReferences(DependencyDescriptor dependency);27 ///
28 /// Return the list of dependencies (as virtual path) of the given module.29 /// If any of the dependency returned in the list is updated, a component depending 30 /// on the assembly produced for the module must be re-compiled.31 /// For example, Razor or WebForms views needs to be recompiled when a dependency of a module changes.32 /// 33 IEnumerable
GetVirtualPathDependencies(DependencyDescriptor dependency);34 }

  从上面接口可以看到Loader有很多方法,它们具有什么功能?

  根据接口,可以将其方法分为以下几类:
  ● 拓展模块:
    ○ 探测:ExtensionProbeEntry Probe(ExtensionDescriptor descriptor);
    ○ 加载:ExtensionEntry Load(ExtensionDescriptor descriptor);
  ● 模块引用:
    ○ 探测:IEnumerable<ExtensionReferenceProbeEntry> ProbeReferences(ExtensionDescriptor extensionDescriptor);
    ○ 加载:Assembly LoadReference(DependencyReferenceDescriptor reference);
  ● 激活与停用:
    ○ 模块激活:void ExtensionActivated(ExtensionLoadingContext ctx, ExtensionDescriptor extension);
    ○ 模块停用:void ExtensionDeactivated(ExtensionLoadingContext ctx, ExtensionDescriptor extension);
    ○ 模块删除:void ExtensionRemoved(ExtensionLoadingContext ctx, DependencyDescriptor dependency);
    ○ 引用激活:void ReferenceActivated(ExtensionLoadingContext context, ExtensionReferenceProbeEntry referenceEntry);
    ○ 引用停用:void ReferenceDeactivated(ExtensionLoadingContext context, ExtensionReferenceProbeEntry referenceEntry);
  ● 监控:void Monitor(ExtensionDescriptor extension, Action<IVolatileToken> monitor);
  ● 编译需要的引用:IEnumerable<ExtensionCompilationReference> GetCompilationReferences(DependencyDescriptor dependency);
  ● 依赖路径支持:IEnumerable<string> GetVirtualPathDependencies(DependencyDescriptor dependency);  

  按照上面的分类,可以大致猜测Loader的使用过程:探测(模块和引用)---->加载(模块和引用)----->激活引用----->激活模块。然后其它接口用于管理拓展的启用和停用,以及编译时的一些辅助。

  从前一篇文章告诉我们的是Orchard的模块激活是以"Module.txt"文件作为输入然后输出一个System.Type的列表。之前通过Folder已经搜索了所有相关目录,解析每一个目录下的Module.txt或Theme.txt文件,生成了一个Extension Descriptor对象列表。

  现在Orchard将通过这些对象使用Loader来探测实际模块以及实际引用的相关信息:
  先放上代码(位于ExtensionLoaderCoordinator类型的CreateLoadingContext方法):

1        Logger.Information("Probing extensions"); 2             var availableExtensionsProbes1 = _parallelCacheContext 3                 .RunInParallel(availableExtensions, extension =>  4                     _loaders.Select(loader => loader.Probe(extension)).Where(entry => entry != null).ToArray()) 5                 .SelectMany(entries => entries) 6                 .GroupBy(entry => entry.Descriptor.Id); 7  8             var availableExtensionsProbes = _parallelCacheContext 9                 .RunInParallel(availableExtensionsProbes1, g =>10                     new { Id = g.Key, Entries = SortExtensionProbeEntries(g, virtualPathModficationDates)})11                 .ToDictionary(g => g.Id, g => g.Entries, StringComparer.OrdinalIgnoreCase);12             Logger.Information("Done probing extensions");13 14             var deletedDependencies = previousDependencies15                 .Where(e => !availableExtensions.Any(e2 => StringComparer.OrdinalIgnoreCase.Equals(e2.Id, e.Name)))16                 .ToList();17 18             // Collect references for all modules19             Logger.Information("Probing extension references");20             var references = _parallelCacheContext21                 .RunInParallel(availableExtensions, extension => _loaders.SelectMany(loader => loader.ProbeReferences(extension)).ToList())22                 .SelectMany(entries => entries)23                 .ToList();24             Logger.Information("Done probing extension references");

  从上面代码可以看到它做了三件事:1、探测所有的拓展信息。2、根据路径修改时间对探测信息进行排序。3、探测所有引用信息。

探测拓展信息:

1 .RunInParallel(availableExtensions, extension => 2                     _loaders.Select(loader => loader.Probe(extension)).Where(entry => entry != null).ToArray())

  根据代码可以看出,对于每一个可用拓展,需要通过所有loader的探测:

  1. CoreExtensionLoader:很直接的判断是否来至于Core目录下,然后直接组建ExtensionProbeEntry。

1     public override ExtensionProbeEntry Probe(ExtensionDescriptor descriptor) { 2             if (Disabled) 3                 return null; 4  5             if (descriptor.Location == "~/Core") { 6                 return new ExtensionProbeEntry { 7                     Descriptor = descriptor, 8                     Loader = this, 9                     Priority = 100, // Higher priority because assemblies in ~/bin always take precedence10                     VirtualPath = "~/Core/" + descriptor.Id,11                     VirtualPathDependencies = Enumerable.Empty
(),12 };13 }14 return null;15 }

    2. PrecompiledExtensionLoader:根据Id(模块名称)获取程序集文件路径,并组建ExtensionProbeEntry。它的依赖列表也是当前程序集。

1 ... 2        Logger.Information("Probing for module '{0}'", descriptor.Id); 3  4             var assemblyPath = GetAssemblyPath(descriptor); 5             if (assemblyPath == null) 6                 return null; 7  8             var result = new ExtensionProbeEntry { 9                 Descriptor = descriptor,10                 Loader = this,11                 VirtualPath = assemblyPath,12                 VirtualPathDependencies = new[] { assemblyPath },13             };14 15             Logger.Information("Done probing for module '{0}'", descriptor.Id);16 ...

    3. DynamicExtensionLoader:与PrecompiledExtensionLoader相似,只不过这里获取的是模块的csproj文件。但是这里要注意的是它的依赖信息通过GetDependencies方法获取了相关的文件信息。

1         Logger.Information("Probing for module '{0}'", descriptor.Id); 2  3             string projectPath = GetProjectPath(descriptor); 4             if (projectPath == null) 5                 return null; 6  7             var result = new ExtensionProbeEntry { 8                 Descriptor = descriptor, 9                 Loader = this,10                 VirtualPath = projectPath,11                 VirtualPathDependencies = GetDependencies(projectPath).ToList(),12             };13 14             Logger.Information("Done probing for module '{0}'", descriptor.Id);    15 ---16  public ProjectFileDescriptor Parse(TextReader reader) {17             var document = XDocument.Load(XmlReader.Create(reader));18             return new ProjectFileDescriptor {19                 AssemblyName = GetAssemblyName(document),20                 SourceFilenames = GetSourceFilenames(document).ToArray(),21                 References = GetReferences(document).ToArray()22             };23         }24 private static string GetAssemblyName(XDocument document) {25             return document26                 .Elements(ns("Project"))27                 .Elements(ns("PropertyGroup"))28                 .Elements(ns("AssemblyName"))29                 .Single()30                 .Value;31         }32 33         private static IEnumerable
GetSourceFilenames(XDocument document) {34 return document35 .Elements(ns("Project"))36 .Elements(ns("ItemGroup"))37 .Elements(ns("Compile"))38 .Attributes("Include")39 .Select(c => c.Value);40 }41 42 private static IEnumerable
GetReferences(XDocument document) {43 var assemblyReferences = document44 .Elements(ns("Project"))45 .Elements(ns("ItemGroup"))46 .Elements(ns("Reference"))47 .Where(c => c.Attribute("Include") != null)48 .Select(c => {49 string path = null;50 XElement attribute = c.Elements(ns("HintPath")).FirstOrDefault();51 if (attribute != null) {52 path = attribute.Value;53 }54 55 return new ReferenceDescriptor {56 SimpleName = ExtractAssemblyName(c.Attribute("Include").Value),57 FullName = c.Attribute("Include").Value,58 Path = path,59 ReferenceType = ReferenceType.Library60 };61 });62 63 var projectReferences = document64 .Elements(ns("Project"))65 .Elements(ns("ItemGroup"))66 .Elements(ns("ProjectReference"))67 .Attributes("Include")68 .Select(c => new ReferenceDescriptor {69 SimpleName = Path.GetFileNameWithoutExtension(c.Value),70 FullName = Path.GetFileNameWithoutExtension(c.Value),71 Path = c.Value,72 ReferenceType = ReferenceType.Project73 });74 75 return assemblyReferences.Union(projectReferences);76 }
View Code

  获取依赖信息,实际上就是解析csproj这个xml文件。

    4. RawThemeExtensionLoader:这个Loader比较特殊,它用于加载~/themes目录下没有代码的主题。如果存在csproj和bin目录下的程序集都会被忽略。

1        // Temporary - theme without own project should be under ~/themes 2             if (descriptor.Location.StartsWith("~/Themes",StringComparison.InvariantCultureIgnoreCase)) { 3                 string projectPath = _virtualPathProvider.Combine(descriptor.Location, descriptor.Id, 4                                            descriptor.Id + ".csproj"); 5  6                 // ignore themes including a .csproj in this loader 7                 if ( _virtualPathProvider.FileExists(projectPath) ) { 8                     return null; 9                 }10 11                 var assemblyPath = _virtualPathProvider.Combine(descriptor.Location, descriptor.Id, "bin",12                                                 descriptor.Id + ".dll");13 14                 // ignore themes with /bin in this loader15                 if ( _virtualPathProvider.FileExists(assemblyPath) )16                     return null;17 18                 return new ExtensionProbeEntry {19                     Descriptor = descriptor,20                     Loader = this,21                     VirtualPath = descriptor.VirtualPath,22                     VirtualPathDependencies = Enumerable.Empty
(),23 };24 }

    5. ReferencedExtensionLoader:引用拓展比较特殊,因为引用拓展是直接被根项目引用的模块项目,这些模块当根项目编译时,它们也会被编译并放置到根项目的bin目录下。

1           var assembly = _buildManager.GetReferencedAssembly(descriptor.Id); 2             if (assembly == null) 3                 return null; 4  5             var assemblyPath = _virtualPathProvider.Combine("~/bin", descriptor.Id + ".dll"); 6  7             return new ExtensionProbeEntry { 8                 Descriptor = descriptor, 9                 Loader = this,10                 Priority = 100, // Higher priority because assemblies in ~/bin always take precedence11                 VirtualPath = assemblyPath,12                 VirtualPathDependencies = new[] { assemblyPath },13             };

注:这里BulidManager中是通过一个DefaultAssemblyLoader来加载程序集的,DefaultAssemblyLoader类型自身维护了一个ConcurrentDictionary<string, Assembly> _loadedAssemblies字典,用于通过程序集的Short Name来存取程序集,如果程序集不存在时就通过程序集的Full Name、Short Name甚至通过程序集解析器来解析出程序集的名称,最后通过这些名称依次通过.net的Assembly调用Load(name)的方法加载返回并对结果进行缓存。

Orchard中有三种名称解析器:AppDomainAssemblyNameResolver、OrchardFrameworkAssemblyNameResolver以及GacAssemblyNameResolver。 它们分别从AppDomain、OrchardFramework依赖的DefaultAssemblyLoader中以及.Net全局程序集中查找程序集。

  从上面的分析可知,它主要的操作是通过ExtensionDescriptor来获取对应模块的程序集路径和优先级,并以当前的加载器打包,以便后续用于加载对应程序集。最终ExtensionProbeEntry是以描述Id进行分组的,意味这同一个拓展存在多个探测实体(来自于不同Loader,主要是有不同的优先级以及路径修改时间)。

探测信息排序:

  这里主要是将上面获取的ExtensionProbeEntry,针对每一个Key(即ExtensionDescriptor.Id)通过优先级来分组,然后取出优先级最高的组,如果改组中存在多个项(如预编译加载器和动态加载器都探测到相应模块,且它们优先级一致)时,根据它们依赖路径的最新修改时间再次进行排序(上面创建ExtensionProbeEntry时VirtualPathDependencies一般为程序集路径,但是动态加载器是所有依赖文件路径,句话说只要任意文件的修改日期大于编译库的信息,那么动态加载器对应的探测实体就会排在前面优先使用)。

1         private IEnumerable
SortExtensionProbeEntries(IEnumerable
entries, ConcurrentDictionary
virtualPathModficationDates) { 2 // All "entries" are for the same extension ID, so we just need to filter/sort them by priority+ modification dates. 3 var groupByPriority = entries 4 .GroupBy(entry => entry.Priority) 5 .OrderByDescending(g => g.Key); 6 7 // Select highest priority group with at least one item 8 var firstNonEmptyGroup = groupByPriority.FirstOrDefault(g => g.Any()) ?? Enumerable.Empty
(); 9 10 // No need for further sorting if only 1 item found11 if (firstNonEmptyGroup.Count() <= 1)12 return firstNonEmptyGroup;13 14 // Sort by last modification date/loader order15 return firstNonEmptyGroup16 .OrderByDescending(probe => GetVirtualPathDepedenciesModificationTimeUtc(virtualPathModficationDates, probe))17 .ThenBy(probe => probe.Loader.Order)18 .ToList();19 }20 private DateTime GetVirtualPathDepedenciesModificationTimeUtc(ConcurrentDictionary
virtualPathDependencies, ExtensionProbeEntry probe) {21 if (!probe.VirtualPathDependencies.Any())22 return DateTime.MinValue;23 24 Logger.Information("Retrieving modification dates of dependencies of extension '{0}'", probe.Descriptor.Id);25 26 var result = probe.VirtualPathDependencies.Max(path => GetVirtualPathModificationTimeUtc(virtualPathDependencies, path));27 28 Logger.Information("Done retrieving modification dates of dependencies of extension '{0}'", probe.Descriptor.Id);29 return result;30 }31 32 private DateTime GetVirtualPathModificationTimeUtc(ConcurrentDictionary
virtualPathDependencies, string path) {33 return virtualPathDependencies.GetOrAdd(path, p => _virtualPathProvider.GetFileLastWriteTimeUtc(p));34 }35 public virtual DateTime GetFileLastWriteTimeUtc(string virtualPath) {36 #if true37 // This code is less "pure" than the code below, but performs fewer file I/O, and it 38 // has been measured to make a significant difference (4x) on slow file systems.39 return File.GetLastWriteTimeUtc(MapPath(virtualPath));40 #else41 var dependency = HostingEnvironment.VirtualPathProvider.GetCacheDependency(virtualPath, new[] { virtualPath }, DateTime.UtcNow);42 if (dependency == null) {43 throw new Exception(string.Format("Invalid virtual path: '{0}'", virtualPath));44 }45 return dependency.UtcLastModified;46 #endif47 }
View Code

探测引用:

  引用探测是探测阶段的最后一步,它将查找所有模块的引用信息。

1 .RunInParallel(availableExtensions, extension => _loaders.SelectMany(loader => loader.ProbeReferences(extension)).ToList())

      1. CoreExtensionLoader:基类的ProbeReferences方法将被调用,返回一个空集合。

1      public virtual IEnumerable
ProbeReferences(ExtensionDescriptor descriptor) {2 return Enumerable.Empty
();3 }

      2. DynamicExtensionLoader:通过解析csproj文件获取的reference信息,创建ExtensionReferenceProbeEntry。

1         public override IEnumerable
ProbeReferences(ExtensionDescriptor descriptor) { 2 if (Disabled) 3 return Enumerable.Empty
(); 4 5 Logger.Information("Probing references for module '{0}'", descriptor.Id); 6 7 string projectPath = GetProjectPath(descriptor); 8 if (projectPath == null) 9 return Enumerable.Empty
();10 11 var projectFile = _projectFileParser.Parse(projectPath);12 13 var result = projectFile.References.Select(r => new ExtensionReferenceProbeEntry {14 Descriptor = descriptor,15 Loader = this,16 Name = r.SimpleName,17 VirtualPath = _virtualPathProvider.GetProjectReferenceVirtualPath(projectPath, r.SimpleName, r.Path)18 });19 20 Logger.Information("Done probing references for module '{0}'", descriptor.Id);21 return result;22 }

      3. PrecompiledExtensionLoader:找到模块程序集路径,将该路径下的所有除了模块程序集以外的.dll文件全部获取路径,并创建ExtensionReferenceProbeEntry。

1         public override IEnumerable
ProbeReferences(ExtensionDescriptor descriptor) { 2 if (Disabled) 3 return Enumerable.Empty
(); 4 5 Logger.Information("Probing references for module '{0}'", descriptor.Id); 6 7 var assemblyPath = GetAssemblyPath(descriptor); 8 if (assemblyPath == null) 9 return Enumerable.Empty
();10 11 var result = _virtualPathProvider12 .ListFiles(_virtualPathProvider.GetDirectoryName(assemblyPath))13 .Where(s => StringComparer.OrdinalIgnoreCase.Equals(Path.GetExtension(s), ".dll"))14 .Where(s => !StringComparer.OrdinalIgnoreCase.Equals(Path.GetFileNameWithoutExtension(s), descriptor.Id))15 .Select(path => new ExtensionReferenceProbeEntry {16 Descriptor = descriptor,17 Loader = this,18 Name = Path.GetFileNameWithoutExtension(path),19 VirtualPath = path20 } )21 .ToList();22 23 Logger.Information("Done probing references for module '{0}'", descriptor.Id);24 return result;25 }

    4. RawThemeExtensionLoader:使用基类方法,获取空列表。

    5. ReferencedExtensionLoader:使用基类方法,获取空列表。

  这里要说明一下,因为Core模块和被引用的模块是被根目录直接引用的,所以在编译时会将模块所依赖的库一并复制到根项目的bin目录中,所以无需额外处理它们。预编译和动态编译的模块的引用信息分别从bin目录下或csproj文件中提取。

引用后续的处理就是根据模块和引用名称进行了分组,以便后续使用。

小结:以上过程主要目的是为了创建一个ExtensionLoadingContext用于后续加载。

1 return new ExtensionLoadingContext {2                 AvailableExtensions = sortedAvailableExtensions,3                 PreviousDependencies = previousDependencies,4                 DeletedDependencies = deletedDependencies,5                 AvailableExtensionsProbes = availableExtensionsProbes,6                 ReferencesByName = referencesByName,7                 ReferencesByModule = referencesByModule,8                 VirtualPathModficationDates = virtualPathModficationDates,9 }

 

转载于:https://www.cnblogs.com/selimsong/p/6085181.html

你可能感兴趣的文章
java.sql.Timestamp cannot be cast to java.sql.Date
查看>>
JS代码大全-2
查看>>
linux install ftp server
查看>>
C# 使用 Abot 实现 爬虫 抓取网页信息 源码下载
查看>>
嵌入式软件设计第8次实验报告
查看>>
NP难问题求解综述
查看>>
算法和数据结构(三)
查看>>
看一下你在中国属于哪个阶层?
查看>>
在iOS 8中使用UIAlertController
查看>>
js获取ip地址,操作系统,浏览器版本等信息,可兼容
查看>>
Ubuntu下的eclipse安装subclipse遇到没有javahl的问题...(2天解决了)
查看>>
Cadence Allegro 如何关闭铺铜(覆铜)shape的显示和设置shape显示模式–allegro小技巧...
查看>>
Atcoder Grand Contest 004 题解
查看>>
MFC中 给对话框添加背景图片
查看>>
alter database databasename set single_user with rollback IMMEDIATE 不成功问题
查看>>
idea 系列破解
查看>>
Repeater + Resources 列表 [原创][分享]
查看>>
c# Resolve SQlite Concurrency Exception Problem (Using Read-Write Lock)
查看>>
dependency injection
查看>>
WCF揭秘——使用AJAX+WCF服务进行页面开发
查看>>