Wednesday, September 10, 2008
#
In Enterprise Library for Microsoft (EntLib v3.1), you can use the WMI TraceListener (WMITraceListener class) to send logging events/statistics to the Windows Performance Monitor application. Steps:
- Enable Instrumentation on the host: Start / All Programs / Microsoft Patterns & Practices / Enteprise Library 3.1 - May 2007 / Install Instrumentation
- With Enterprise Library Configuration, configure your app.config or web.config to use the WMI TraceListener.
- With Enterprise Library Configuration, add Instrumentation
- Edit the resulting config file (from previous steps) to set the instrumentation properties to "true":
true" eventLoggingEnabled="true" wmiEnabled="true" />
- If you have subclassed LogEntry, make sure any custom typed Attributes are decorated with [IgnoreMember] (you'll have to have System.Management as a Reference)
- Run your program
- Add a new Counter in Windows Performance Monitor: Performance Object "Enterprise Library Logging Counter"; Select counters from list; Select your program's instance.
Troubleshooting:
- If "Enterprise Library Logging Counter" isn't available in Performance Monitor, then you didn't Install Instrumentation in EntLib.
- If you can't select an instance while adding the Counter, then your program probably isn't running, or something is preventing it from being instrumentable
- If the application event log contains errors suggesting that you use IgnoreMember, then you are logging a subclassed EventLog that has Attributes with special types (the event log error will say which one). Or change the type of you attributes to string, int, etc... to avoid the problem.
Sunday, August 24, 2008
#
Idea
YetAnotherForum.NET (YAF.NET) on SharePoint, sounds like a great idea!
(The main focus of this article is partitioning data between YAF.NET forums based on SharePoint site.)
It sounds like YAF.NET might already handle authentication of users arriving through Active Directory (ie: auto register), but I don't want just a YAF.NET install next to my SharePoint system. My needs are to have YAF.NET be a feature that can be deployed across hundreds (thousands?) of SharePoint sites without intermingling the content (Topics/Posts) between sites.
It seemed like it might be a weekend effort, but with family activities, other personal projects and the size of the effort, I've decided that I just won't have the time to port YetAnotherForum.NET to SharePoint 2007 (WSSv3). I will share my findings though.
Approach #1
Inversion of Control, decoupling the data access mechanism from the application so a new data store (ie: SharePoint) could be injected. The approach would be to take all the DB access methods in YAF.NET, and make them use an interface, and put the existing DB access code into a class to satisfy that data interface. The phase 1 result would be a code refactored YAF.NET that works exactly the same as it did.
The second phase of the port would be to create another implementation of the data access interface that utilizes SharePoint lists instead of the SQL database. Plug in that new interface, and the YAF.NET should work fine without knowing that its running on SharePoint. It should be possible to satisfy all the database needs of YAF.NET with SharePoint lists and CAML for access. The only need that might not work is YAF.NET's free-text search capabilities.
Data will need to be compartmentalized into the SharePoint sites that YAF.NET was enabled for. This will help application scaling and content ownership. There are some practical data limits to SharePoint's core list capabilities, but utilizing separate YAF.NET lists per SharePoint site avoid those issues. The YAF.NET SharePoint data access would have to utilize the lists for the current site to make this work, would be pretty easy once the rest was in place.
Example YAF.NET loading from SharePoint site context:
http://hostname/sites/TestSite/_layouts/YAF/default.aspx
The SharePoint site at "/sites/TestSite" would have the normal YAF.NET data sources:

[...]
The entire YAF.NET for SharePoint system would be packaged as a SharePoint site collection or web scoped feature in a SharePoint solution file. That means that once the solution has been added to your SharePoint system, site administrators could go to Site Features (or Site Collection Features) and enable that feature. The initialization of the feature would run, creating the proper lists and default data.
This approach sounds like the best SharePoint solution, but without in-depth knowledge of YAF.NET, it feels like 3-4 weeks worth of development and testing.
Approach #2
A compromise solution to porting YAF.NET to SharePoint would be to partition the data for SharePoint sites within the current YAF.NET SQL database. By adding a new "Context" column to all existing tables, then slightly modifying all the SQL in YAF.NET, the system could function within the context of the SharePoint site it was accessed from. This approach simulates different data stores for each SharePoint site, but does not change the native source YAF.NET already uses, so the approach is less complex.
This approach sounds like a reasonable SharePoint solution, and avoids some of the questions about SharePoint list data scalability issues by continuing to use native SQL Server. Because this approach is generally many simple modifications, it feels more like 1-3 weeks worth of development and testing.
I started some work down this approach to flush out the main issues. Some of these steps are required for either approach...
Live in SharePoint Layouts
YAF.NET would need to function in the common SharePoint "layouts" directory. This is where common SharePoint functions/systems need to live, and they are given the opportunity to be "aware" of the site context that they are invoked from. This part was quick and easy.
Misc:
In \web.config, I enabled session state, and switched trust level to full. Neither is a great idea for SharePoint, but worked for my development purposes...
pages enablesessionstate="true" ...
trust level="Full" originUrl=""
For this url to work:
http://hostname/sites/TestSite/_layouts/YAF/default.aspx
...YAF.NET needs to be copied to:
C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS\YAF
(I put the YAF.NET binaries into the webroot/bin for testing)
Config.cs needs to know about site context:
///
/// Determine the site context from the http request.
/// /
/// /sites/TestSite
///
static public string UrlContextPath
{
get
{
string scriptUrl = HttpContext.Current.Request.ServerVariables["SCRIPT_NAME"];
string rawUrl = HttpContext.Current.Request.RawUrl;
int pos = rawUrl.ToUpper().IndexOf(scriptUrl.ToUpper());
if (pos <>
{
return "/";
}
return "/" + rawUrl.Substring(0, pos);
}
}
UrlBuilder.cs needs to build urls with site context:
public string BuildUrl(string url)
{
return string.Format("{0}{1}?{2}",
Config.UrlContextPath.Substring(1),
HttpContext.Current.Request.ServerVariables["SCRIPT_NAME"],
url
);
//return string.Format("{0}?{1}",HttpContext.Current.Request.ServerVariables["SCRIPT_NAME"],url);
}
Partition Data by Site Context
This is the BIGGEST porting task, requiring changes to all 31 database tables, 1 view, 162 stored procedures, and all corresponding code invocations of SQL and stored procedures.
Add the Context field to all database tables. For example:
IF NOT EXISTS (SELECT 1
FROM sysobjects
WHERE id = Object_id(N'yaf_AccessMask')
AND Objectproperty(id,N'IsUserTable') = 1)
CREATE TABLE dbo.yaf_AccessMask (
[AccessMaskID] INT IDENTITY NOT NULL,
[Context] NVARCHAR(255) NOT NULL,
[BoardID] INT NOT NULL,
[Name] NVARCHAR(50) NOT NULL,
[Flags] INT NOT NULL CONSTRAINT DF_yaf_AccessMask_Flags DEFAULT (0))
GO
IF NOT EXISTS (SELECT 1
FROM syscolumns
WHERE id = Object_id('yaf_AccessMask')
AND name = 'Context')
BEGIN
ALTER TABLE dbo.yaf_AccessMask
ADD Context NVARCHAR(255) NOT NULL DEFAULT '/'
END
GO
Modify all SQL and Stored Procedures to use the new Context field. For example:
CREATE PROCEDURE [dbo].[yaf_accessmask_delete](
@AccessMaskID INT,
@Context NVARCHAR(255) = '/')
AS
BEGIN
DECLARE @flag INT
SET @flag = 1
IF EXISTS (SELECT 1
FROM yaf_ForumAccess
WHERE AccessMaskID = @AccessMaskID)
OR EXISTS (SELECT 1
FROM yaf_UserForum
WHERE AccessMaskID = @AccessMaskID)
SET @flag = 0
ELSE
DELETE FROM yaf_AccessMask
WHERE AccessMaskID = @AccessMaskID
AND Context = @Context
SELECT @flag
END
GO
Modify all invocations to SQL and Stored Procedures to use the new Context field. For example:
static public bool accessmask_delete( object accessMaskID )
{
using ( SqlCommand cmd = new SqlCommand( "yaf_accessmask_delete" ) )
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue( "@AccessMaskID", accessMaskID );
cmd.Parameters.AddWithValue("@Context", Config.UrlContextPath);
return (int)ExecuteScalar(cmd) != 0;
}
}
With a few of these changes in effect, YAF.NET continued to function properly, which helped prove the viability of the approach, but also proved that it would take a non-trivial amount of time to complete. I did not continue the effort due to my lack of availability.
Additional Issues:
Web Part - These approaches are targeted at making YAF.NET work as a companion to SharePoint, not fully integrate it. An expectation of any SharePoint admin would be how to drop a dynamic view of the forum onto a SharePoint Web Part Page. Luckily YAF.NET is constructed with web user controls, which translate pretty easily into web parts. Look at SmartPart WebPart (or other sample code) for how to load a normal web user control in a Web Part container.
Isolation - Some elements of YAF.NET need to be shared rather than isolated between SharePoint web sites. Data such as Users, Permissions and Access Masks are some examples of data that might need to be shared across all SharePoint sites (especially users!)
Overlapping domains - Some elements of YAF.NET need to pull data from SharePoint rather than handled by YAF.NET. For instance, Users and Permissions should come from SharePoint, otherwise SharePoint admins will need to setup users/permissions TWICE (once for SharePoint sites, and another time for YAF.NET).
Default data - Some data is provisioned during the one-time YAF.NET install. Some amount of data provisioning may need to occur once per SharePoint site. During setup/initialization. For instance, the AccessMask is setup with some default permissions. If data in YAFF.NET is merely partitioned between SharePoint sites, that one-time data initialization will only benefit one SharePoint site.
Tuesday, June 24, 2008
#
General List management:
- SharePoint lists are basically just Database Tables (or excel worksheets). You have control over how many columns exist, and what their types are.
- SharePoint columns are generally set to these core types: 1 line of text, multiple lines of text (w/ or wo/ rich formatting), numerical, dates, choices, user/group assignment, lookup
- SharePoint choice columns are flat choice lists, but a hierarchy can be simulated by having choices such as: Food, Food:Groceries, Food:Restaurant, Food:CoffeeShop. That would allow you to choose "Food" as the parent node OR "Food:CoffeeShop" if you wanted to choose the child node. Maintaining the values in a choice column requires hard-core list maintenance.
- SharePoint choice columns may allow "Fill-in" choices if desired where the user is given a chance to choose a value from the drop down selector, OR type in their own value. The fill-in choice is NOT automatically added to the choice list for general use (the user would have to type the exact same value each time they wanted to use it).
- SharePoint list attachments and folders can be enabled or disabled on any list. List attachments let you attach files to individual list items.
- SharePoint list views can present vastly different presentations based on the same underlying items stored in the list. One view is the default for the list, others may be created as necessary. All views must be configured with: what columns to display; what order to display the columns; how to sort/group/filter the items; other presentation and content choices. This is a huge benefit as it typically allow designs utilizing one underlying list for many purposes rather than requiring the difficult management of many seperate lists.
- SharePoint list web parts are used to put lists on a web part page. Each list web part could present a different view of the same underlying list. This is a huge benefit for presenting the user with many different views and rollups of one list on a single page.
Advanced List management:
- SharePoint lookup columns are like choice columns, but refer to another SharePoint list for values instead. Maintaining the values in a lookup column merely requires adding an entry to the referenced list. The referenced list could be ANY type of list: Custom List, Tasks, Contacts, Calendar, Document Library, …
- SharePoint Site Columns and Content Types allow list maintainers to define a set of fields that need to be entered and managed together in a list that supports several Content Types. For example, we could define 5 columns for a "design tool" Content Type and 8 columns for a "runtime tool" Content Type. When someone adds/edits/views a "design tool" item, the system will display the 5 columns defined for it. When manipulating a "runtime tool" item, it will use the associated 8 columns. This approach works best when there are overlapping sets of column definitions, otherwise if there were no commonality, they might as well just be separate lists.
- SharePoint list permissions can be set to allow/restrict access to the list as a whole.
- SharePoint list item permissions can be set to allow granular permissions per item. This must be maintained manually per item (high burdon).
- SharePoint list item permissions can be set to allow: Read access to everyone, or just a user's own items; Edit access to all, none, or just a user's own items.
- SharePoint list versioning and approval handling can be enabled or disabled on any list.
- SharePoint workflows are possible, but are pretty advanced.
Thursday, April 03, 2008
#
When to call Dispose() on your SharePoint WSSv3 site and web objects seems like an ornery topic. Dispose should be releasing memory and resources, and not actually writing or destroying any data, so not calling it properly could cause leaks of memory or other resources. Here is the general rule I've learned.
1) If you instantiate the object with “new” or OpenWeb, you need to Dispose() of it.
2) If you indirectly instantiate the web object by using SPSite.RootWeb, then you need to Dispose() of it... but not in the way you might think.
This is what SPSite.RootWeb is doing:
public SPWeb RootWeb
{
get
{
if (this.m_rootWeb == null)
{
this.m_rootWeb = this.OpenWeb(this.ServerRelativeUrl);
this.m_rootWebCreated = true;
}
return this.m_rootWeb;
}
}
So now it makes sense as to why SPSite.RootWeb might need special care to Dispose() just as if you had called OpenWeb... because you effectively did, since SPSite.Dispose() doesn't dispose the web object.
BUT, you could Dispose() of the SPWeb, OR the SPSite object to release that SPWeb. If you Dispose() the SPWeb, the SPSite gets a little confused, but not tragically (m_rootWeb ends up null, but m_rootWebCreated remains true). If you Dispose() of the SPSite instead, everything is properly Dispose()ed and all state remains consistent. With the varied advice on when and how to Dispose of the SPWeb from the SPSite.RootWeb, it is lucky that doing extra Dispose() calls don't do anything, or even error.
Look at this debugging session. Notice how the internal state of the “site“ object changes based on the worst case scenario of getting the RootWeb twice and Dispose()ing it between the references to RootWeb (I was trying to cause an error by calling Dispose() then getting the RootWeb again). The most interesting revelations are that Dispose()ing the Web immediately affects the state of its parent SPSite, and that Dispose()ing the SPSite automatically, and completely, cleans up the SPWeb gotten via RootWeb.
static void Main(string[] args)
{
SPSite site = new SPSite("http://localhost");
SPWeb web = null;
try
{
// site.m_rootWeb == null
// site.m_rootWebCreated == false
ShowDetails(site);
// site.m_rootWeb == null
// site.m_rootWebCreated == true
web = site.RootWeb;
}
finally
{
// site.m_rootWeb == web
// site.m_rootWebCreated == true
// web.m_closed = false
site.Dispose();
// site.m_rootWeb == null
// site.m_rootWebCreated == false
// web.m_closed = true
web.Dispose();
// site.m_rootWeb == null
// site.m_rootWebCreated == false
// web.m_closed = true
web.Dispose(); // Extra Disposes do nothing
// site.m_rootWeb == null
// site.m_rootWebCreated == false
// web.m_closed = true
}
}
static void ShowDetails(SPSite site)
{
SPWeb web = null;
try
{
// site.m_rootWeb == null
// site.m_rootWebCreated == false
web = site.RootWeb;
}
finally
{
// site.m_rootWeb == web
// site.m_rootWebCreated == true
// web.m_closed = false
web.Dispose(); // This is where SPSite's state first gets confused.
// site.m_rootWeb == null
// site.m_rootWebCreated == true
// web.m_closed = true
}
}
NOTE: The above example intentionally uses the long hand try/finally blocks to be clear about the point. The better way to code Dispose() is with the C# “using” directive to implicitly call Dispose(), such as “using (SPWeb web = site.RootWeb) { do something; }”