WSS 列表和数据库同步
将外部数据源与 SharePoint 列表同步。
引言
Windows SharePoint Services (WSS) 和 Microsoft Office SharePoint Server (MOSS) 是提供有用信息和分析信息的优秀工具。使这些信息对最终用户可见的基本组件之一是列表。然后可以使用这些列表根据指定条件过滤数据,从而专注于关键信息,形成不同的数据视图。MOSS 还可以使用这些列表作为创建关键绩效指标 (KPI) 的基础。
SharePoint 有什么问题?
使用 SharePoint 显示信息和数据的最大问题是,SharePoint 只能在了解信息的情况下显示信息,例如文档库中的文档列表。然而,大多数企业并不使用 SharePoint 来存储所有数据;它们拥有外部系统或业务线 (LOB) 系统,这些系统可能在 SharePoint 之前就已经存在,或者使用具有更具体目标的其他系统,例如订单处理系统或仓库管理系统。
将外部数据导入 SharePoint
本文将简要讨论各种将外部数据暴露给 SharePoint 的方法,以便在 SharePoint 列表中使用,但主要重点将放在创建一种可重用且可扩展的方法,该方法结合了所有方法的最佳优点。
由于 MOSS 是 WSS 的扩展,因此本文将使用“SharePoint”一词来同时表示两者。特定于 MOSS 的功能将另行注明。
业务数据目录
业务数据目录 (BDC) 是 MOSS 的一项功能,它使用 XML 文件定义和公开外部数据源。
有许多文章详细介绍了 BDC,因此我们仅对其进行简要了解以便熟悉。
尽管 BDC 定义文件可以手动创建,但这会是一项繁琐的任务,如下所示,因此最好使用许多可用工具之一。Microsoft 在 SharePoint Server 2007 SDK 中提供了业务数据目录定义编辑器。
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
- <LobSystem xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.microsoft.com/office/2006/03/
BusinessDataCatalog BDCMetadata.xsd"
Type="Database" Version="1.0.0.0" Name="Northwind"
xmlns="http://schemas.microsoft.com/office/2006/03/BusinessDataCatalog">
- <LobSystemInstances>
- <LobSystemInstance Name="Northwind_Instance">
- <Properties>
<Property Name="rdbconnection Data Source" Type="System.String">dbserver</Property>
<Property Name="rdbconnection Initial Catalog" Type="System.String">Northwind</Property>
<Property Name="rdbconnection User ID" Type="System.String">sa</Property>
<Property Name="DatabaseAccessProvider"
Type="Microsoft.Office.Server.ApplicationRegistry.
SystemSpecific.Db.DbAccessProvider">SqlServer</Property>
<Property Name="AuthenticationMode"
Type="Microsoft.Office.Server.ApplicationRegistry.
SystemSpecific.Db.DbAuthenticationMode">PassThrough</Property>
</Properties>
</LobSystemInstance>
</LobSystemInstances>
- <Entities>
- <Entity EstimatedInstanceCount="10000" Name="Employees">
- <Identifiers>
<Identifier TypeName="System.Int32" Name="EmployeeID" />
</Identifiers>
- <Methods>
- <Method Name="Find_Employees">
+ <Properties>
<Property Name="RdbCommandType"
Type="System.Data.CommandType, System.Data, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">Text</Property>
<Property Name="RdbCommandText"
Type="System.String">Select "EmployeeID","LastName",
"FirstName","Title", "TitleOfCourtesy","BirthDate",
"HireDate","Address","City","Region",
"PostalCode", "Country","HomePhone","Extension",
"Photo","Notes","ReportsTo","PhotoPath"
from Employees where EmployeeID=@EmployeeID</Property>
</Properties>
+ <Parameters>
- <Parameter Direction="In" Name="@EmployeeID">
<TypeDescriptor
TypeName="System.Int32, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"
IdentifierName="EmployeeID" Name="EmployeeID" />
</Parameter>
- <Parameter Direction="Return" Name="@Employees">
- <TypeDescriptor
TypeName="System.Data.IDataReader, System.Data, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"
IsCollection="true" Name="Reader">
- <TypeDescriptors>
- <TypeDescriptor
TypeName="System.Data.IDataRecord, System.Data, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"
Name="Record">
- <TypeDescriptors>
<TypeDescriptor
TypeName="System.Int32, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"
IdentifierName="EmployeeID" Name="EmployeeID" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="LastName" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="FirstName" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"
Name="Title" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="TitleOfCourtesy" />
<TypeDescriptor
TypeName="System.DateTime, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="BirthDate" />
<TypeDescriptor
TypeName="System.DateTime, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="HireDate" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="Address" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="City" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="Region" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="PostalCode" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="Country" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="HomePhone" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="Extension" />
<TypeDescriptor
TypeName="System.Byte[], mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="Photo" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="Notes" />
<TypeDescriptor
TypeName="System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="ReportsTo" />
<TypeDescriptor
TypeName="System.String, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="PhotoPath" />
</TypeDescriptors>
</TypeDescriptor>
</TypeDescriptors>
</TypeDescriptor>
</Parameter>
</Parameters>
+ <MethodInstances>
<MethodInstance
Type="SpecificFinder"
ReturnParameterName="@Employees"
ReturnTypeDescriptorName="Reader"
ReturnTypeDescriptorLevel="0"
Name="Find_Employees_Instance" />
</MethodInstances>
</Method>
+ <Method Name="FindAll_Employees">
- <Properties>
<Property Name="RdbCommandType"
Type="System.Data.CommandType, System.Data, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">Text</Property>
<Property Name="RdbCommandText"
Type="System.String">Select "EmployeeID" from Employees</Property>
</Properties>
- <Parameters>
- <Parameter Direction="Return" Name="@Employees">
- <TypeDescriptor
TypeName="System.Data.IDataReader, System.Data, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
IsCollection="true" Name="Reader">
- <TypeDescriptors>
- <TypeDescriptor
TypeName="System.Data.IDataRecord, System.Data, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
Name="Record">
- <TypeDescriptors>
<TypeDescriptor
TypeName="System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
IdentifierName="EmployeeID"
Name="EmployeeID" />
</TypeDescriptors>
</TypeDescriptor>
</TypeDescriptors>
</TypeDescriptor>
</Parameter>
</Parameters>
- <MethodInstances>
<MethodInstance
Type="IdEnumerator"
ReturnParameterName="@Employees"
ReturnTypeDescriptorName="Reader"
ReturnTypeDescriptorLevel="0"
Name="FindAll_Employees_Instance" />
</MethodInstances>
</Method>
</Methods>
</Entity>
</Entities>
</LobSystem>
此示例针对 Northwind 数据库的 Employees 表,我们可以看到它定义了访问和公开数据所需的必要访问方法和连接字符串信息。由于有许多其他文章涵盖了 BDC 的深度和细微差别,因此我们在此不再赘述。
将 BDC 暴露给 SharePoint
BDC XML 文件通过中央管理网站导入到 MOSS,然后即可在任何网站中使用。这是使用 BDC 的一个缺点;无法限制对特定网站的公开,尽管可以应用权限。
BDC 导入后,可以通过指定自定义列将项目添加到各个列表中。
MOSS 中还有其他 Web 部件可用,例如业务数据列表,也可以使用。
自定义 Web 部件
克服 BDC 限制且不依赖 MOSS 的一种方法是创建自定义 Web 部件,该 Web 部件访问必要数据并以列表方式显示它。SPGridView
控件非常适合此功能;但是,它也有局限性。SPGridView
源自 ASP.NET 的 GridView
控件,因此为 ASP.NET 开发人员提供了熟悉的功能。
然而,此方法会暴露两个问题。一、必须手动完成所有操作;所有排序、筛选和显示都必须进行编码。二、它不是 SharePoint 列表,不能用于创建不同的视图或在其他列表中使用这些列。
自定义列表方法
显然,一种可以利用上述方法的最佳功能并利用内置 SharePoint 列表功能的方法将是有利的。
以编程方式创建 SharePoint 列表
从下面的代码可以看出,创建 SharePoint 列表非常容易。
private void CreateList()
{
using(SPSite site = new SPSite("http://sharepoint.com/MySite"))
{
using(SPWeb web = site.OpenWeb())
{
// Use the Custom List template
SPListTemplate template = web.ListTemplates["Custom List"];
Guid listGuid = web.Lists.Add("ListName", "Description", template);
SPList list = web.Lists[listGuid];
list.Fields.Add("Column1", SPFieldType.Text, true);
// Add other fields to list
list.Update();
web.Update();
}
}
}
创建列表后,可以为列表添加一个视图。
private void CreateView()
{
using(SPSite site = new SPSite("http://sharepoint.com/MySite"))
{
using(SPWeb web = site.OpenWeb())
{
SPList list = web.Lists["List Name"];
SPView view = list.Views["All Items"];
view.ViewFields.Add("View Column1");
// Add other view fields
view.Update();
}
}
}
向列表中添加数据也非常简单。
private void AddDataToList()
{
DataTable dt = new DataTable();
using(SqlConnection conn = new SqlConnection("Connection String"))
{
using(SqlCommand cmd = new SqlCommand("cmd", conn))
{
conn.Open();
dt.Load(cmd.ExecuteReader(CommandBehavior.CloseConnection));
}
}
using(SPSite site = new SPSite("http://sharepoint.com/MySite"))
{
using(SPWeb web = site.OpenWeb())
{
SPList list = web.Lists["List Name"];
foreach(DataRow row in dt.Rows)
{
SPListItem item = list.Items.Add();
item["Column1"] = row["Column1"];
// Repeat for additional columns
item.Update();
}
}
}
}
上面的代码 intended to run on a system that has SharePoint installed, since it uses classes from the SharePoint API. To use these techniques outside of a SharePoint environment, you would need to use the SharePoint Web Services.
WssDatabaseSync
尽管上述方法可以有效使用,但它不够灵活;列、站点和列表是硬编码的,当然,只适用于指定的列表。站点和列表可以被抽象出来,但你仍然需要处理列。
WssDatabaseSync
项目使用类似于 BDC 功能的定义文件,来定义创建 SharePoint 列表和从数据库(如自定义列表方法)填充数据所需的信息。但是,由于这些方法已被抽象,因此可以通过编辑定义文件简单而灵活地将任何数据库源暴露给任何列表。
定义文件
<?xml version="1.0" encoding="utf-8" ?>
<syncProjects>
<syncProject name="SyncProject1">
<syncSource>
<connectionString>Connection string</connectionString>
<source isStoredProc="false">View or Stored proc name</source>
</syncSource>
<syncDestination append="true"
sourceKeyField="EmployeeID" destinationKeyField="EmployeeID">
<site>http://sharepoint.com/MySite</site>
<list>Employees</list>
</syncDestination>
<columns>
<column source="EmployeeID" destination="ID" dataType="Integer"/>
<column source="FirstName" destination="First Name" dataType="Text"/>
<column source="LastName" destination="Last Name" dataType="Text"/>
<column source="Address" destination="Address" dataType="Text"/>
<column source="City" destination="City" dataType="Text"/>
<column source="Today" destination="Date"
dataType="DateTime" timeZone="Central Standard Time"/>
</columns>
</syncProject>
</syncProjects>
syncProject
元素封装了每个项目的设置和定义,name
属性仅用于提高可读性。
syncSource
元素定义了将用于填充列表的数据源。为了减少对恶意 SQL 语句(即 SQL 注入)的暴露,此项目仅使用存储过程或视图。isStoredProc
仅指示给定字符串是否为存储过程。
syncDestination
元素定义了要使用的站点和列表。append
属性决定是追加到列表还是更新列表。sourceKeyField
和 destinationKeyField
属性是可选的,除非 append
为 true
,在这种情况下,它们标识用于比较以更新给定列表项的字段。
column
元素定义了要添加到 SharePoint 列表的列。source
和 destination
属性应该是不言自明的。dataType
属性是任何有效 SPFieldType
枚举值的字符串表示。
已添加的一种特殊列类型是通过将 Today
指定为 source
属性值来表示的。它还利用了可选的 timeZone
属性。添加此项是为了支持来自不同时区的日期。由于 SharePoint 服务器可能与用户处于不同的时区,因此这允许进行调整以获得预期的值。
支持类
使用 LINQ to SQL 的几个内部类用于简化对定义文件的访问。
#region Internal classes
internal class SyncProject
{
…
}
internal class SyncSource
{
public SyncSource(XElement sourceElement)
{
ConnectionString = sourceElement.Element("connectionString").Value;
DataSource = sourceElement.Element("source").Value;
IsStoredProc = Convert.ToBoolean(sourceElement.Element("source").
Attribute("isStoredProc").Value);
}
public string ConnectionString { get; private set; }
public string DataSource { get; private set; }
public bool IsStoredProc { get; private set; }
}
internal class SyncDestination
{
…
}
internal class SyncColumn
{
…
}
#endregion
主处理
代码简单地遍历定义文件中的每个 syncProject
元素,该元素由 SyncProject
类表示。
SyncProjects projects = new SyncProjects();
foreach(SyncProject project in projects.Projects)
{
SyncProject(project);
}
private static void SyncProject(SyncProject project)
{
try
{
SPList list = null;
Console.WriteLine("Attempting to open: " + project.Destination.Site);
using(SPSite site = new SPSite(project.Destination.Site))
{
using(SPWeb web = site.OpenWeb())
{
try
{
Console.WriteLine("Varify list exists...");
list = web.Lists[project.Destination.List];
}
catch(ArgumentException)
{
// Can't find list so just eat exception and create the list
Console.WriteLine("Creating list...");
list = CreateList(web, project.Destination.List, project.Columns);
CreateView(list, project.Columns);
CreateNavigationMenuItem(web, project.Destination.List);
}
// Should be valid by now but check anyway
if(list != null)
{
SyncList(project, list);
}
}
}
}
catch(Exception ex)
{
if(ExceptionPolicy.HandleException(ex, "Default Policy"))
throw;
}
}
正如我们在此处看到的,尝试打开作为目标的 SPSite
以及目标 Web。当我们查找指定的列表时,有趣的地方在于此。由于 SPWeb.Lists
属性公开的 SPListsCollection
不包含 Contains
这样的方法,因此我们需要处理尝试访问不存在的列表时抛出的 ArguementException
。
如果列表不存在,当然需要创建它。
private static SPList CreateList(SPWeb web, string listName, List<SyncColumn> columns)
{
SPList list = null;
try
{
Guid listGuid = web.Lists.Add(listName, listName,
web.ListTemplates["Custom List"]);
list = web.Lists[listGuid];
SPFieldCollection fields = list.Fields;
foreach(SyncColumn col in columns)
{
SPField field = new SPField(fields, col.DataType, col.Destination);
list.Fields.Add(field);
}
list.Update();
web.Update();
}
catch(ArgumentException ex)
{
Console.WriteLine("Create list failed");
if(ExceptionPolicy.HandleException(ex, "Default Policy"))
throw;
}
return list;
}
此方法没有什么特别之处,只需遍历为 SyncProject
定义的列,并将 SPField
添加到 SPList
的 SPFieldCollection
中。
同步列表与数据库
列表创建并添加到站点后,下一步是将其数据填充进去。
第一步是获取数据。使用定义文件中指定的 SQLConnection
创建连接,并使用定义文件中指定的存储过程或视图创建 SQLCommand
。返回的 DataTable
会返回给调用方法。
private static DataTable GetDataSource(SyncProject project)
{
Console.WriteLine("Connecting to database...");
DataTable dt = new DataTable();
using(SqlConnection conn = new SqlConnection(project.Source.ConnectionString))
{
string cmdString = string.Empty;
if(project.Source.IsStoredProc)
cmdString = project.Source.DataSource;
else
cmdString = "SELECT * FROM " + project.Source.DataSource;
using(SqlCommand cmd = new SqlCommand(cmdString, conn))
{
if(project.Source.IsStoredProc)
cmd.CommandType = CommandType.StoredProcedure;
conn.Open();
dt.Load(cmd.ExecuteReader(CommandBehavior.CloseConnection));
}
}
return dt;
}
获取数据后,我们只需遍历 DataTable
中的每个 DataRow
,并将 SPListItem
与 DataColumn
进行匹配。
private static void SyncList(SyncProject project, SPList list)
{
DataTable dt = GetDataSource(project);
Console.Write("Creating items...");
foreach(DataRow row in dt.Rows)
{
Console.Write(".");
SPListItem item = null;
if(project.Destination.ShouldAppend)
item = FindItem(list, project.Destination.DestinationKeyField,
row[project.Destination.SourceKeyField]);
else
item = list.Items.Add();
foreach(SyncColumn col in project.Columns)
{
if(col.Source.ToUpper().CompareTo("TODAY") == 0)
item[col.Destination] = GetToday(col.TimeZone);
else
item[col.Destination] = row[col.Source] ==
DBNull.Value ? null : row[col.Source];
}
item.Update();
}
Console.WriteLine("{0} items added", dt.Rows.Count);
}
这里有两个有趣的地方:查找(如果需要)要更新的 SPItem
,以及处理特殊的 Today 列。
为了查找 SPItem
,我们只需遍历列表中的项目并将其值与我们要查找的内容进行比较。如果找到项目,则返回该项目;否则,将新的 SPListItem
添加到列表中。
private static SPListItem FindItem(SPList list, string destinationField, object value)
{
foreach(SPListItem item in list.Items)
{
if(item[destinationField].ToString() == value.ToString())
return item;
}
return list.Items.Add();
}
为了处理 Today 字段,我们使用 TimeZone
值属性将当前 DateTime
转换为正确的时区。
private static string GetToday(string timeZone)
{
DateTime dt = TimeZoneInfo.ConvertTime(DateTime.Now,
TimeZoneInfo.FindSystemTimeZoneById(timeZone));
return dt.ToString();
}
创建导航链接
添加一个链接到新创建的列表到快速链接是一个很好的便利步骤。
private static void CreateNavigationMenuItem(SPWeb web, string listName)
{
// Create a navigation item for this list
string url = "Lists/" + listName + "/AllItems.aspx";
SPNavigationNode navNode = new SPNavigationNode(listName, url);
foreach(SPNavigationNode node in web.Navigation.QuickLaunch)
{
// Find the Lists node
if(node.Title == "Lists")
{
bool menuFound = false;
// Check if menu item already exists
foreach(SPNavigationNode item in node.Children)
{
if(item.Url == navNode.Url)
{
menuFound = true;
break;
}
}
// If the menu wasn't found then add it
if(!menuFound)
node.Children.AddAsLast(navNode);
}
}
}
结论
此解决方案没有太困难或特别之处,关键在于理解问题和可能的解决方案,并将这些组件组合起来解决问题。在研究了这个问题之后,我找不到任何一种解决方案,或者至少是一种免费的解决方案,能够为用户提供所需的灵活性和易用性。我认为它可能对需要类似解决方案以及深入了解 SharePoint 编程的其他用户有用。
附加功能
此解决方案可以添加一个额外的功能,即编辑器来创建和编辑定义文件,以进一步将内部细节与最终用户隔离。
历史
- 08/11/30 – 初始发布。