Using Disposable Windows SharePoint Services Objects

英文原文: http://msdn.microsoft.com/zh-cn/library/aa973248.aspx

这里中文主要使用Google Translate, 如果你英语更好建议看原文! 实在不行可以看code,我就是这样Google Translate +code! 大概还是懂起文章的意思了!

主要讲的是我们在使用sharepoint对象模型的时候,是否及时释放资源,好的代码和坏的代码风格比较! 不保证翻译很准确,请大家指正!

 

介绍使用一次性的Windows SharePoint Services对象

在Windows SharePoint Services 3.0对象模型中的对象作为界面与Windows SharePoint Services数据的工作。通常,开发人员调入对象模型来读取数据或写入新的数据存储在Windows SharePoint服务。

Windows SharePoint Services对象模型包含对象实现了IDisposable接口。你必须采取预防措施时,使用这些对象,以避免他们长期在Microsoft .NET Framework的内存中!

具体来说,你应该明确地处置这些SharePoint对象实现IDisposable接口你完成的时候使用它们。

您使用SharePoint对象广泛,例如,在SharePoint网站使用自定义Web部件,您可能会导致通过SharePoint对象不处理以下异常行为时,你已经完成了他们。

1.频繁回收的Windows SharePoint Services应用程序池,尤其是在用电高峰

2.应用程序崩溃,由于堆在调试器中出现的腐败
3.高级Microsoft Internet信息服务(IIS)的工作进程内存使用
4.糟糕的系统和应用性能

 

本文作为一个以处理和SharePoint对象实现IDispose处理的正确程序指南。在本文所讨论的问题也是由SharePoint处置标记检查器工具,免费下载该方案作为检查编码做法的原因,因为处理不当和SharePoint对象处理内存泄漏您的程序集可用。

 

几个Windows SharePoint Services对象,主要是SPSite类和SPWeb类的对象,几个创建为管理对象。然而,这些对象使用非托管代码和内存来执行他们的大部分工作。对象的管理部分是比非托管部分较小。因为较小的管理部分不放在垃圾收集器内存压力,垃圾回收器不会及时释放从内存中的对象。该对象的非托管内存的大量使用会导致不寻常前面描述的一些行为。调用应用程序在Windows SharePoint Services的IDisposable的对象的工作必须处理的对象的应用程序结束时使用它们。您不应该依赖于垃圾收集器会自动从内存中释放他们。

寻找对象的不正确处理完毕

你可以通过问以下问题的正确处理可能存在的对象:

  1. 您的应用程序池回收频繁,特别是根据(假设该应用程序池设置回收内存时达到阈值)重物?
    内存大小应该800 MB- 1.5 GB的(假设至少2 GB的RAM)。设置回收应用程序池越接近1 GB的发生提供了最好的结果,但实验,以确定哪些设置适合您的环境。如果回收设置太低,你的经验,因为频繁的回收应用程序池的性能问题。如果设置得太高,你的系统遇到性能问题,因为页交换,内存碎片,以及其他事项。
  2. 请问您的系统表现不佳,尤其是在负载很重的情况?随着内存使用量开始增加,系统必须赔偿,例如,通过寻呼内存和处理内存碎片。
  3. 请问您的系统崩溃或用户遇到意外的错误,特别是在重负载,如超时或页面没有可用的错误意外的错误?同样,当内存使用量的增加或获取支离破碎,有些功能不能失败,因为他们对其他操作分配内存。在许多情况下,代码不正确处理“内存不足“异常,从而导致虚假或误导性的错误。
  4. 请问您的系统使用自定义或第三方Web部件或自定义应用程序?您可能没有意识到,他们必须处理的SharePoint对象和原因,假定垃圾收集自动执行此功能。然而,这并不是真正的所有情况。

如果你回答是4个Yes,以及一个或更多的其他问题,有一个很好的机会,你的自定义代码不正确处理的项目。

如果您的网站是展示不同寻常的行为之一如前所述,你可以判断一个内存泄漏的原因是由于通过检查对象的诊断日志(在C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\LOGS)有关SPRequest对象。每个实例包含的SPSite和SPWeb一个对SPRequest对象,反过来,包含一个引用到一个非托管COM对象,处理与数据库服务器通信的参考。 Windows SharePoint Services的监察SPRequest对象,在每一个特定的线程,在线程并行存在的数量,并增加了有用的条目下的三种情况记录:

  • 该SPRequest对象的总数超过了配置的阈值。
  • 一个SPRequest对象继续存在并在线程的结束。
  • 一个SPRequest对象是垃圾收集。

第一种情况发生最频繁,特别是如果您的网站使用了八个SPRequest对象的默认阈值。每当SPRequest对象的数量超过此阈值时,下列项目中出现的诊断日志:

潜在的SPRequest对象人数过多(数目的对象)目前尚未发布有关线程的线程数。确保该对象或其母公司(如SPWeb或SPSite对象)正在妥善处理。为此对象分配标识{GUID},

最佳阈值根据不同的性质,您的网站上运行的应用程序。当您的网站正遇到性能问题,您应该监控您的安装的地下物流系统日志,了解许多物件SPRequest网站正在创建的应用程序。这可以帮助您确定您的网站和应用程序的设计是创建过多SPRequest对象。即使对象不正确的处理是不是你的表现问题的原因,您可能需要重新设计你的网站或定制网站应用程序,以降低整体记忆体消耗的SPRequest对象过度增殖造成的。

 

因为默认的门槛很低,可能并不适用于许多网站,你可以改变这种通过编辑以下注册表子项的门槛:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\HeapSettings

LocalSPRequestWarnCount = 所需的阈值

在您确定的对象不正确的处置可能造成SPRequest对象增殖,不必要地增加了内存占用您的网站,您可以通过以下二项前瞻性的不正确处置的具体实例。两个消息,以至于内存被因为不正确的SharePoint对象出售浪费案件,都涉及到的数量和SPRequest对象的状态在一个线程:

… 跳过一点, 直接看代码处理这段, 还是看代码情切点…

编码技术来确保对象的处置

你也可以使用某些编码技术,以确保对象的处理。这些技术包括使用在您的代码如下:

  • Dispose method
  • using clause
  • try, catch, and finally blocks
  • Dispose  VS . Close方法处理对比

    为SPWeb对象和功能相同的方式SPSite对象的Dispose和Close方法。 Dispose方法调用对象的Close方法。我们建议调用Dispose方法,而不是关闭,因为SPWeb和的SPSite对象实现了IDisposable接口,和标准的。NET Framework垃圾收集调用Dispose方法释放从内存与对象关联的所有资源。

     

    //不要这样做。SPWeb自动调用Dispose()
    
    using( SPWeb web = SPControl.GetContextWeb(HttpContext.Current)) { ... }
    
    
    
    //SPContext对象是由SharePoint管理框架,不应在代码中明确处置。这是真实的也由SPContext.Site,SPContext.Current.Site,SPContext.Web和SPContext.Current.Web返回的SPSite和SPWeb对象。
    
    
    //在下面的代码示例中,一个SPSite对象被实例化,但不处理,因为运行时可确保只有OpenWeb()返回SPWeb对象
    
    void CombiningCallsLeak()
    
    {
    
        using (SPWeb web = new SPSite(SPContext.Current.Web.Url).OpenWeb())
    
        {
    
            // ... New SPSite will be leaked. 在new spsite 将溢出,下面嵌套using
    
        } // SPWeb object web.Dispose() automatically called.
    
    }
    
    
    
    //您可以通过嵌套using在另一份声明中使用解决此一问题。
    
    void CombiningCallsBestPractice()
    
    {
    
        using (SPSite siteCollection = new SPSite(SPContext.Current.Web.Url))
    
        {
    
            using (SPWeb web = siteCollection.OpenWeb())
    
            {
    
            //Perform operations on site.
    
            } // SPWeb object web.Dispose() automatically called.
    
        }  // SPSite object siteCollection.Dispose() automatically called.
    
    }
    
    
    
    //如果您不是SPSite对象上执行任何操作,你可以写这个更简洁,如下面的代码示例。
    
    void CombiningCallsBestPractice()
    
    {
    
    	//这样是不是更简洁一点呢? 我现在就经常这样写了...
    
        using (SPSite siteCollection = new SPSite(SPContext.Current.Web.Url))
    
        using (SPWeb web = siteCollection.OpenWeb())
    
            {
    
            //Perform operations on site.
    
            } // SPWeb object web.Dispose() automatically called; SPSite object 
    
              // siteCollection.Dispose() automatically called.
    
    }

    在其他情况下,必须建立自己的尝试,catch和finally块。最明显的例子是情况下,您需要处理的异常,因此必须包含一个catch块。以下部分提供了有关何时以及如何使用try,捕捉方针,finally块。

    The try, catch, and finally Blocks

    使用try,catch和finally块,显然是有道理的,当您需要处理异常。代码中的任何一个try / catch块应该有一个理事最后条款,确保实现IDisposable对象处置。请注意,在下面的代码示例中,您应该填写的代码,处理异常的catch块。切勿空的catch块。另外,请注意为空测试前处理的最佳做法。

    String str;
    
    SPSite oSPSite = null;
    
    SPWeb oSPWeb = null;
    
    
    
    try
    
    {
    
       oSPSite = new SPSite("http://server");
    
       oSPWeb = oSPSite.OpenWeb(..);
    
    
    
       str = oSPWeb.Title;
    
    }
    
    catch(Exception e)
    
    {
    
       //Handle exception, log exception, etc.
    
    }
    
    finally
    
    {
    
       if (oSPWeb != null)
    
         oSPWeb.Dispose();
    
    
    
       if (oSPSite != null)
    
          oSPSite.Dispose();
    
    }

    try和finally语句块或使用须避免潜在的漏洞,当您建立一个foreach块内可支配的对象,如下面的代码示例所示。

    public static void SPSiteCollectionForEachBestPractice()
    
    {
    
         string sUrl = "http://spvm";
    
     
    
          using (SPSite siteCollectionOuter = new SPSite(sUrl))
    
         {
    
             SPWebApplication webApp = siteCollectionOuter.WebApplication;
    
             SPSiteCollection siteCollections = webApp.Sites;
    
    
    
                      SPSite siteCollectionInner = null;
    
                      foreach (siteCollectionInner in siteCollections)
    
                 {
    
                          try  //Should be first statement after foreach.
    
                          {
    
                              Console.WriteLine(siteCollectionInner.Url);
    
                              //Exception occurs here.
    
                          }
    
                          finally
    
                          {
    
                              if(siteCollectionInner != null)
    
                              siteCollectionInner.Dispose();//释放资源
    
                     }
    
                 }
    
             }
    
         } // SPSite object siteCollectionOuter.Dispose() automatically called. 这里使用using,将自动释放资源
    
     }

     

    Response.Redirect with try, catch, and finally Blocks and using Statements

    finally块执行在try块后调用Response.Redirect。 Response.Redirect最终生成一个ThreadAbortException。引发此异常时,运行时执行的所有线程结束前终于块。但是,因为finally块可以做一个无限的计算或取消ThreadAbortException,也不能保证该线程将结束。因此,在任何重定向或加工转移可以发生,你必须处理的对象。如果您的代码必须重定向,实施的方式类似于下面的代码例子。

    String str;
    
    SPSite oSPSite = null;
    
    SPWeb oSPWeb = null;
    
    
    
    try
    
    {
    
       oSPSite = new SPSite("http://server");
    
       oSPWeb = oSPSite.OpenWeb(..);
    
    
    
       str = oSPWeb.Title;
    
       if(bDoRedirection)
    
       {
    
           if (oSPWeb != null)
    
              oSPWeb.Dispose();
    
        
    
           if (oSPSite != null)
    
              oSPSite.Dispose();
    
    
    
           Response.Redirect("newpage.aspx");
    
       }
    
    }
    
    catch(Exception e)
    
    {
    
    }
    
    finally
    
    {
    
       if (oSPWeb != null)
    
         oSPWeb.Dispose();
    
    
    
       if (oSPSite != null)
    
          oSPSite.Dispose();
    
    }
    
    
    
    //因为using每次运行生成一个finally块,每当你使用Response.Redirect在Using里,确保妥善处置对象。下面的代码示例显示了如何做到这一点。
    
    using (SPSite oSPSite = new SPSite("http://server"))
    
    using (SPWeb oSPWeb = oSPSite.OpenWeb(..))
    
    {
    
        if (bDoRedirection)
    
            Response.Redirect("newpage.aspx");
    
    }
    
    
    
    

     

    //良好的编码习惯#1 明确调用Dispose()
    
    void CreatingSPSiteExplicitDisposeNoLeak()
    
    {
    
        SPSite siteCollection = null;
    
        try
    
        {
    
            siteCollection = new SPSite("http://moss");
    
        }
    
        finally
    
        {
    
            if (siteCollection != null)
    
                siteCollection.Dispose();
    
        }
    
    }
    
    //良好的编码习惯#2 自动调用Dispose()
    
    CreatingSPSiteWithAutomaticDisposeNoLeak()
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
        } // SPSite object siteCollection.Dispose() is called automatically.
    
    }
    
    //好的编码习惯
    
    void OpenWebNoLeak()
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
            using (SPWeb web = siteCollection.OpenWeb())
    
            {
    
            } // SPWeb object web.Dispose() automatically called.
    
        }  // SPSite object siteCollection.Dispose() automatically called.
    
    }
    
    

    不共享任何SPRequest对象(和扩展名的任何对象,它包含一个对SPRequest对象的引用)跨线程。任何编码技术,股份两个或多个线程之间的SPRequest对象,或者创建一个线程上一SPRequest对象,它在另一个处置,不支持。这意味着你可以不存储任何对象拥有一个在静态变量SPRequest对象的引用。不这样做,因此,商店的SharePoint对象实现IDisposable的静态变量(如SPWeb或的SPSite)。

     

    SPSite Objects

    本节介绍中,新的SPSite对象返回,必须加以处置的情况。在一般情况下,任何时间调用应用程序使用SPSite构造器,它应该调用Dispose方法时,使用完对象。如果SPSite对象是从GetContextSite获得,调用应用程序不应处理的对象。由于SPWeb的SPSite对象和保持一个内部列表是根据这种方式,对象可能会导致SharePoint对象模型的行为不可预测的处置。在内部,Windows SharePoint Services中列举了这个清单完成后,页面的对象妥善处理。

    //不好的编码做法
    
    void SPSiteCollectionAddLeak()
    
    {
    
        SPWebApplication webApp = new SPSite("http://moss").WebApplication;
    
        SPSiteCollection siteCollections = webApp.Sites;
    
        SPSite siteCollection = siteCollections.Add("sites/myNewSiteCollection", "DOMAIN\\User", 
    
          "[email protected]");
    
        // SPSite siteCollection leak. 没有释放
    
    }
    
    //良好的编码做法
    
    void SPSiteCollectionAddNoLeak()
    
    {
    
        SPWebApplication webApp = new SPSite("http://moss").WebApplication;
    
        SPSiteCollection siteCollections = webApp.Sites;
    
        using (SPSite siteCollection = siteCollections.Add("sites/myNewSiteCollection", "DOMAIN\\User", 
    
          "[email protected]"))
    
        {
    
        } // SPSite object siteCollection.Dispose() automatically called.
    
    }
    /**
    
    * SPSiteCollection [ ] Index Operator
    
    *该SPSiteCollection[]索引操作符返回每个访问新的SPSite对象。一个实例是创建的SPSite对象,即使是已经访问。下面的代码示例演示SPSite对象正确的处理。
    
    */
    
    
    
    //Bad Coding Practice #1
    
    void SPSiteCollectionIndexerLeak()
    
    {
    
        using (SPSite siteCollectionOuter = new SPSite("http://moss"))
    
        {
    
            SPWebApplication webApp = siteCollectionOuter.WebApplication;
    
            SPSiteCollection siteCollections = webApp.Sites;
    
    
    
            SPSite siteCollectionInner = siteCollections[0];
    
            // SPSite siteCollectionInner leak. 
    
        } // SPSite object siteCollectionOuter.Dispose() automatically called.
    
    }
    
    
    
    //Bad Coding Practice #2
    
    void SPSiteCollectionForEachLeak()
    
    {
    
        using (SPSite siteCollectionOuter = new SPSite("http://moss"))
    
        {
    
            SPWebApplication webApp = siteCollectionOuter.WebApplication;
    
            SPSiteCollection siteCollections = webApp.Sites;
    
    
    
            foreach (SPSite siteCollectionInner in siteCollections)
    
            {
    
                // SPSite siteCollectionInner leak.这里没有释放
    
            }
    
        } // SPSite object siteCollectionOuter.Dispose() automatically called.
    
    }
    
    
    
    
    
    //Good Coding Practice #1
    
    void SPSiteCollectionIndexerNoLeak()
    
    {
    
        using (SPSite siteCollectionOuter = new SPSite("http://moss"))
    
        {
    
            SPSite siteCollectionInner = null;
    
            try
    
            {
    
                SPWebApplication webApp = siteCollectionOuter.WebApplication;
    
                SPSiteCollection siteCollections = webApp.Sites;
    
    
    
                siteCollectionInner = siteCollections[0];
    
            }
    
            finally
    
            {
    
                if (siteCollectionInner != null)
    
                    siteCollectionInner.Dispose();
    
            }
    
        } // SPSite object siteCollectionOuter.Dispose() automatically called.
    
    }
    
    
    
    //Good Coding Practice #2
    
    void SPSiteCollectionForEachNoLeak()
    
    {
    
        using (SPSite siteCollectionOuter = new SPSite("http://moss"))
    
        {
    
            SPWebApplication webApp = siteCollectionOuter.WebApplication;
    
            SPSiteCollection siteCollections = webApp.Sites;
    
    
    
            foreach (SPSite siteCollectionInner in siteCollections)
    
            {
    
                try
    
                {
    
                    // ...
    
                }
    
                finally
    
                {
    
                    if(siteCollectionInner != null)
    
                        siteCollectionInner.Dispose();
    
                }
    
            }
    
        } // SPSite object siteCollectionOuter.Dispose() automatically called.
    
    }
    /**
    
    *	SPSite.AllWebs Property (SPWebCollection)
    
    *	本节描述了AllWebs属性集合,要求SPWeb对象访问后要关闭的方法,属性或其他操作。
    
    *	该SPSite.AllWebs.Add方法创建并返回一个SPWeb对象。你应该从SPSite.AllWebs.Add处置返回的任何SPWeb对象。
    
    **/
    
    
    
    //Bad Coding Practice
    
    void AllWebsAddLeak()
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
            SPWeb web = siteCollection.AllWebs.Add("site-relative URL");
    
            // SPWeb object leaked.
    
        }  // SPSite object siteCollection.Dispose() automatically called. 
    
    }
    
    
    
    
    
    //Good Coding Practice
    
    void AllWebsAddNoLeak()
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
            using (SPWeb web = siteCollection.AllWebs.Add("site-relative URL"))
    
            {
    
            } // SPWeb object web.Dispose() automatically called.
    
        }  // SPSite object siteCollection.Dispose() automatically called. 
    
    }
    
    
    
    /*
    
    *	SPWebCollection.Add Method
    
    *	该SPWebCollection.Add方法创建并返回一个SPWeb对象,需要加以处置。
    
    *
    
    **/
    
    
    
    //Bad Coding Practice
    
    void SPWebCollectionAddLeak(string strWebUrl)
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
            using (SPWeb outerWeb = siteCollection.OpenWeb())
    
            {
    
                SPWebCollection webCollection = siteCollection.AllWebs; // No AllWebs leak just getting reference.
    
                SPWeb innerWeb = webCollection.Add(strWebUrl);  // Must dispose of innerWeb.
    
                // innerWeb leak.
    
            } // SPWeb object outerWeb.Dispose() automatically called.
    
        }  // SPSite object siteCollection.Dispose() automatically called. 
    
    }
    
    
    
    //Good Coding Practice
    
    void SPWebCollectionAddNoLeak(string strWebUrl)
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
            using (SPWeb outerWeb = siteCollection.OpenWeb())
    
            {
    
                SPWebCollection webCollection = siteCollection.AllWebs; // No AllWebs leak just getting reference.
    
                using (SPWeb innerWeb = webCollection.Add(strWebUrl))
    
                {
    
                    //...
    
                }
    
            } // SPWeb object outerWeb.Dispose() automatically called.
    
        }  // SPSite object siteCollection.Dispose() automatically called. 
    
    }
    
    
    
    
    
    /**
    
    *	SPSite.AllWebs [ ] Index Operator
    
    *	该SPSite.AllWebs[]索引操作符返回一个新的SPWeb实例在每次访问时间。创建一个对象的索引操作过程中,即使是已经访问该对象。如果没有正确关闭,下面的代码示例中的一个离开的.NET Framework垃圾回收器SPWeb对象。
    
    *
    
    **/
    
    //Bad Coding Practice
    
    void AllWebsForEachLeak()
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
            using (SPWeb outerWeb = siteCollection.OpenWeb())
    
            {
    
                foreach (SPWeb innerWeb in siteCollection.AllWebs)
    
                {
    
                    // Explicitly dispose here to avoid out of memory leaks with large number of SPWeb objects.
    
                }
    
            } // SPWeb object outerWeb.Dispose() automatically called.
    
        }  // SPSite object siteCollection.Dispose() automatically called. 
    
    }
    
    
    
    
    
    //Good Coding Practice #1
    
    void AllWebsForEachNoLeakOrMemoryOOM()
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
            using (SPWeb outerWeb = siteCollection.OpenWeb())
    
            {
    
                foreach (SPWeb innerWeb in siteCollection.AllWebs)
    
                {
    
                    try
    
                    {
    
                        // ...
    
                    }
    
                    finally
    
                    {
    
                        if(innerWeb != null)
    
                            innerWeb.Dispose();
    
                    }
    
                }
    
            } // SPWeb object outerWeb.Dispose() automatically called.
    
        }  // SPSite object siteCollection.Dispose() automatically called. 
    
    }
    
    
    
    //Good Coding Practice #2
    
    void AllWebsIndexerNoLeak()
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
            using (SPWeb web = siteCollection.AllWebs[0])
    
            {
    
            } // SPWeb object web.Dispose() automatically called.
    
        }  // SPSite object siteCollection.Dispose() automatically called. 
    
    }
    
    
    
    
    
    /**
    
    *	SPSite.OpenWeb and SPSite. SelfServiceCreateSite Methods
    
    *	该OpenWeb方法和SelfServiceCreateSite方法(所有签名)创建一个SPWeb对象并返回给调用者。这个新的对象不是存储在SPSite对象,
    
    *	也不是任何地方弃置在SPSite类。基于这个原因,你应该通过这些方法处理创建的任何对象。
    
    *
    
    **/
    
    //Bad Coding Practice
    
    void OpenWebLeak()
    
    {
    
        using (SPWeb web = new SPSite(SPContext.Current.Web.Url).OpenWeb())
    
        {
    
            // SPSite leaked !
    
        } // SPWeb object web.Dispose() automatically called.
    
    }
    
    
    
    //Good Coding Practice
    
    void OpenWebNoLeak()
    
    {
    
        using (SPSite siteCollection = new SPSite("http://moss"))
    
        {
    
            using (SPWeb web = siteCollection.OpenWeb())
    
            {
    
            } // SPWeb object web.Dispose() automatically called.
    
        }  // SPSite object siteCollection.Dispose() automatically called.
    
    }
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    还有很多很多类似的,记得使using哦,太多了,不想复制过来了, 待以后在整理上来吧,今天就这么多!

     

    还有几篇关于sharepoint的文章,推荐阅读:

     

    SharePoint对象模型性能考量 (推荐阅读)

    使用 SharePoint 对象模型时的常见代码问题 (来源MSDN)

    在sharepoint中 使用SPSiteDataQuery来进行跨列表查询

    不要忘记SPUtility类

     

    Technorati 标签: , , ,

    你可能感兴趣的:(SharePoint)