RSS 2.0 Feed

Tuesday, December 04, 2007

工作重心转移到SharePoint上来已经有很长一段时间了。随着使用的深入,我常常会在SharePoint中遇到一些貌似神秘的行为,我称之为SharePoint的小秘密。如果有时间,我会将自己的一些观察和研究与大家分享。

刚才看到kaneboy在他的blog里讨论了迁移网站集所使用内容数据库的方法。一个Web Application支持多个Content DB是SharePoint一个很核心的功能,比如通过Content DB备份,迁移和恢复SharePoint网站,网站的性能等等,许多重要的功能都与此相关。

那么,当我们在一个Web Application中使用了多个Content DB时,我们会遇到一个有趣的问题,即我所谓的SharePoint的小秘密。我们知道,在建立新的Site Collection的时候,我们是无法选择该Site Collection使用哪个Content DB的,那么我们的Site Collection到底被建在了哪个Content DB中呢?

很久没写Blog,突然不太习惯.Text的编辑方式了。这个问题的答案等我用Word写好,再贴进来吧。正好也卖个关子,大家有兴趣可以测试一下,找找答案。

posted @ | Feedback (6) | Filed Under [ SharePoint ]

Tuesday, September 20, 2005

有客户问我,怎么拿到应用到WebPart上的Audiences。一想很简单,还不就是用AudienceManage通过Audience所对应的GUID来Get吗。谁知道一写才发现了问题,与某个WebPart对应的GUID去哪里找到呢?难道要去Query database?不信WebPart类有这么傻。

查了半天终于发现,WebPart的IsIncludedFilter属性包含了所有应用到WebPart上的Audiences的GUID,它们使用逗号分隔。知道了这个就简单了。

private void button1_Click(object sender, System.EventArgs e) { // Get PortalContext. TopologyManager tm = new TopologyManager(); PortalSite ps = tm.PortalSites[new Uri("http://testsrv")]; PortalContext pc = PortalApplication.GetContext(ps); // Get all WebParts. SPSite site = new SPSite("http://testsrv"); SPWeb web = site.OpenWeb(); SPWebPartCollection wpcollection = web.GetWebPartCollection("default.aspx", Storage.Shared); // Get all audiences for each webpart. AudienceManager am = new AudienceManager(pc); foreach(WebPart wp in wpcollection) { if(wp.IsIncludedFilter == string.Empty) { Debug.WriteLine("No Target Audience was selected for " + wp.Title); } else { // IsIncludedFilter contains a comma delimited list of audience GUID, // which format is "'guid1','guid2'" string[] GUIDs = wp.IsIncludedFilter.Split(','); foreach(string GUID in GUIDs) { Audience au = am.GetAudience(new Guid(GUID.Substring(1, GUID.Length - 2))); if(au != null) { Debug.WriteLine("Target Audience, " + au.AudienceName + ", was selected for " + wp.Title); ArrayList membership = au.GetMembership(); if(membership != null) { for(int i = 0; i < membership.Count; i++) { Debug.WriteLine(" --" + ((UserInfo)membership[i]).PreferredName); Debug.WriteLine(" --" + ((UserInfo)membership[i]).Email); Debug.WriteLine(" --" + ((UserInfo)membership[i]).NTName); } } } } } } }

posted @ | Feedback (11) | Filed Under [ 工作 ]

Monday, July 04, 2005

根据InfoPath team blog里介绍的File Attachment Control的内容,写了两个类来处理文件的编码与解码。

public class InfoPathAttachmentEncoder { private string base64EncodedFile = string.Empty; private string fullyQualifiedFileName; /// <summary> /// Creates a encoder to create an InfoPath attachement string /// </summary> /// <param name="fullyQualifiedFileName"></param> public InfoPathAttachmentEncoder(string fullyQualifiedFileName) { if (fullyQualifiedFileName == string.Empty) throw new ArgumentException("Must specify file name", "fullyQualifiedFileName"); if (!File.Exists(fullyQualifiedFileName)) throw new FileNotFoundException("File does not exist: " + fullyQualifiedFileName, fullyQualifiedFileName); this.fullyQualifiedFileName = fullyQualifiedFileName; } /// <summary> /// Returns a Base64 encoded string /// </summary> /// <returns>String</returns> public string ToBase64String() { if (base64EncodedFile != string.Empty) return base64EncodedFile; // This memory stream will hold the InfoPath file attachment buffer before Base64 encoding MemoryStream ms = new MemoryStream(); // get the file information using (BinaryReader br = new BinaryReader(File.Open(fullyQualifiedFileName, FileMode.Open, FileAccess.Read, FileShare.Read))) { string fileName = Path.GetFileName(fullyQualifiedFileName); uint fileNameLength = (uint)fileName.Length + 1; byte[] fileNameBytes = Encoding.Unicode.GetBytes(fileName); using (BinaryWriter bw = new BinaryWriter(ms)) { // Write InfoPath attachment signature bw.Write(new byte[] { 0xC7, 0x49, 0x46, 0x41 }); // Write the default header information bw.Write((uint)0x14); // size bw.Write((uint)0x01); // version bw.Write((uint)0x00); // reserved // Write the file size bw.Write((uint)br.BaseStream.Length); // Write the size of the file name bw.Write((uint)fileNameLength); // Write the file name (Unicode encoded) bw.Write(fileNameBytes); // Write the file name terminator (which is two nulls in Unicode) bw.Write(new byte[] {0,0}); // Iterate through the file reading data and writing it to the outbuffer byte[] data = new byte[64*1024]; int bytesRead = 1; while (bytesRead > 0) { bytesRead = br.Read(data, 0, data.Length); bw.Write(data, 0, bytesRead); } } } // This memorystream will hold the Base64 encoded InfoPath attachment MemoryStream msOut = new MemoryStream(); using (BinaryReader br = new BinaryReader(new MemoryStream(ms.ToArray()))) { // Create a Base64 transform to do the encoding ToBase64Transform tf = new ToBase64Transform(); byte[] data = new byte[tf.InputBlockSize]; byte[] outData = new byte[tf.OutputBlockSize]; int bytesRead = 1; while (bytesRead > 0) { bytesRead = br.Read(data, 0, data.Length); if (bytesRead == data.Length) tf.TransformBlock(data, 0, bytesRead, outData, 0); else outData = tf.TransformFinalBlock(data, 0, bytesRead); msOut.Write(outData, 0, outData.Length); } } msOut.Close(); return base64EncodedFile = Encoding.ASCII.GetString(msOut.ToArray()); } }
public class InfoPathAttachmentDecoder
{ private const int SP1Header_Size = 20; private const int FIXED_HEADER = 16; private int fileSize; private int attachmentNameLength; private string attachmentName; private byte[] decodedAttachment; /// <summary> /// Accepts the Base64 encoded string /// that is the attachment /// </summary> public InfoPathAttachmentDecoder(string theBase64EncodedString) { byte [] theData = Convert.FromBase64String(theBase64EncodedString); using(MemoryStream ms = new MemoryStream(theData)) { BinaryReader theReader = new BinaryReader(ms); DecodeAttachment(theReader); } } private void DecodeAttachment(BinaryReader theReader) { //position the reader to get the filesize byte[] headerData = new byte[FIXED_HEADER]; headerData = theReader.ReadBytes(headerData.Length); fileSize = (int)theReader.ReadUInt32(); attachmentNameLength = (int)theReader.ReadUInt32() * 2; byte[] fileNameBytes = theReader.ReadBytes(attachmentNameLength); //InfoPath defaults to UTF8 encoding. Encoding enc = Encoding.Unicode; attachmentName = enc.GetString(fileNameBytes, 0, attachmentNameLength - 2); decodedAttachment = theReader.ReadBytes(fileSize); } public void SaveAttachment(string saveLocation) { string fullFileName = saveLocation; if(!fullFileName.EndsWith(Path.DirectorySeparatorChar.ToString())) { fullFileName += Path.DirectorySeparatorChar; } fullFileName += attachmentName; if(File.Exists(fullFileName)) File.Delete(fullFileName); FileStream fs = new FileStream(fullFileName, FileMode.CreateNew); BinaryWriter bw = new BinaryWriter(fs); bw.Write(decodedAttachment); bw.Close(); fs.Close(); } public string Filename { get{ return attachmentName; } } public byte[] DecodedAttachment { get{ return decodedAttachment; } } }
这样,我们就能根据需要对File Attachment Control来做进一步的控制了。
 

posted @ | Feedback (3) | Filed Under [ 工作 学习 ]

Monday, November 08, 2004

为偶的NotesManager小程序写了一个带滚动条的Label控件,效果看起来象下面这样:

 

这个控件的实现思路说起来很简单,就是使用的一般Windows桌面程序中的窗口(Window)和视口(View)的概念。

考虑到Label本身不需要支持编辑的功能,出于效率的考虑,我在ScrollLabel控件中用一个Bitmap对象来保存Window的所有内容。在OnPaint中,如果发现Bitmap对象是空值(通常是第一次Paint),则根据当前Text的内容判断是否需要显示ScrollBar,并创建Bitmap对象,然后根据View的位置来显示应当显示在界面上的内容。以后每次Paint的时候,都只是根据滚动条的位置来计算View的位置,然后显示相应的Bitmap上的内容即可。当Text或者Font改变时,将原有的Bitmap销毁重新计算即可。整个OnPaint方法看起来象下面这样:

         protected override void OnPaint(PaintEventArgs e)

         {

              base.OnPaint (e);

 

              if(bmpWindow == null)

              {

                   Rectangle drawRect;

 

                   // Calculate the height of the window

                   SizeF size = e.Graphics.MeasureString(this.Text, this.Font);

                   rowHeight = size.Height;

                   float height = (size.Width / ClientSize.Width + 1) * rowHeight;

 

                   // Indicate if the scrollbar need to be shown.

                   if(height > ClientRectangle.Height)

                   {

                       // Recalculate the height of window.

                       height = (size.Width / (ClientSize.Width - scrollWidth)) * rowHeight + ClientSize.Height / 2;

 

                       // Show scrollbar

                       vs.Bounds = new Rectangle(this.ClientSize.Width - scrollWidth, 0,

                            scrollWidth, ClientSize.Height);

                       vs.LargeChange = (int)(ClientSize.Height / rowHeight);

                       vs.Maximum = (int)(height / rowHeight);

                       vs.Visible = true;

 

                       // Create window bitmap.

                       bmpWindow = new Bitmap(ClientSize.Width - scrollWidth, (int)height + 1);

                       drawRect = new Rectangle(0, 0, ClientSize.Width - scrollWidth, (int)height + 1);

                   }

                   else

                   {

                       bmpWindow = new Bitmap(ClientSize.Width, ClientSize.Height);

                       drawRect = this.ClientRectangle;

                   }

 

                   // Paint on the window bitmap.

                   Graphics g = Graphics.FromImage(bmpWindow);

                   g.FillRectangle(new SolidBrush(this.BackColor), drawRect);

                   g.DrawString(this.Text, this.Font, new SolidBrush(this.ForeColor), drawRect);

                   g.Dispose();

              }

 

              // Draw view.

              e.Graphics.DrawImage(bmpWindow, 0, 0 - (int)rowHeight * vs.Value);

         }

这个ScrollLabel的代码可以从这里下载。

不过这里使用的方法也有一些小缺陷,主要是源于目前版本的.NET CF不支持的一些功能。比如,为了得到作为Window的Bitmap的宽度和高度,我使用了MeasureString这个方法。但是目前版本的.NET CF中,MeasureString并不支持按给定宽度度量所需要的高度的功能,只能量出将所有的字画在一行上的宽度和高度。我的解决办法是,用MeasureString得到的高度作为行高,用MeasureString得到的宽度和整个ClientSize的宽度比值作为所有的行数,从而得到Window所需的高度。但是这样一来,就无法顾及由于英文的分词所带来的误差。比如MeasureString这个词,如果恰好在行尾,在Paint的时候它不会被截断,而时候整个的被放到下一行来显示。这样不断的叠加,最终在行数上是会产生一定的误差的。我暂时使用的解决方法是,对我用MeasureString得到的Window高度进行了1/2个ClientSize高度的误差补偿,这样你会发现,ScrollBar在滚动时并不是精确地滚动到最后一行,而是还会向下滚动一段空白区域。在大多数情况下,这段误差补偿是可以解决问题的,但是对于包含很多非常长的单词的情况,可能仍然会有问题。

更新后的NotesManager可以在这里下载。

 

posted @ | Feedback (14) | Filed Under [ 学习 ]

Thursday, November 04, 2004

刚才收到mvm的邮件,说我的上一个post不能加comment,comment的submit按钮不work了,试了一下果然不能submit评论了。

刚才我用上一个post做了个测试,用来试验开心说的避免帖子显示在博客堂首页的方法,开始我以为这是引起问题的原因。Advanced里面的那一堆Checkbox前几个我都试了试,结果发现,似乎取消选中“显示在首页”并不能避免帖子显示在博客堂首页上,倒是帖子不会显示在自己的首页上了;取消选中“发布”的话,帖子不会显示在博客堂首页上,但是同时也不会显示在自己的首页上了,这样的话帖子也就没人能看到了;取消选中“允许评论”的话,帖子上不会有Feedback的链接。现在我选中了所有的选项,但是评论的submit按钮仍然不work。

再仔细想想今天我还改了什么,想起来下午我给自己的blog加了一个google站内搜索,但是测试下来发现,这段站内搜索代码似乎不能用。首先,click了google search按钮之后并没有按预期转到google的页面,而只是将本来的首页刷新了一遍似的。我在自己本地建了个测试页面,放上google的这段代码是可以work的,这说明可能是博客堂本身的代码与它有冲突;其次,google的站内搜索其实还是使用的site关键字,而这个site关键字搜索是不支持搜索类似http://blog.joycode.com/sam1111 这样的URL地址的,只支持类似blog.joycode.com这样的顶级域名,所以其实并不能按我的预期实现对http://blog.joycode.com/sam1111 的站内搜索;

虽说这段站内搜索不Work,我还是将那段代码留在那里了,主要是懒得去删了。刚才想起这会不会是引起不能submit comment的原因呢?因此去把它删了,现在submit comment果然可以用了。看来还真是google的search按钮与comment的submit按钮有冲突呢,难道是使用了相同的ID?

附:耽误了mvm的comment,特附在下面 :)

kaka, 居然不能在vpc里面运行sp的模拟器?

 

那能在vpc里面运行SFC模拟器么?

 

Btw, 无法在http://blog.joycode.com/sam1111/archive/2004/11/04/38033.aspx里面留言,本来想留在那个上面的。我这里居然click submit以后没有反应。很郁闷。

 

20:27更新:

SFC模拟器suppose应该是可以在VPC中运行的吧。SP的模拟器不能运行是因为SP的模拟器本质上是一个WinCE虚拟机,因此不能嵌在其他的Windows虚拟机中运行。虽然没试过,不过我想一定不能在一个Win2k3的虚拟机中再run一个WinXP的虚拟机。而SFC的模拟器应该与Windows虚拟机没什么关系。有空试试先。:)

 

21:05更新:

趁下班之前这段时间作了个测试,在虚拟机中安装VPC2004时报错无法安装虚拟网卡;能成功安装,但无法在WinXP虚拟机中运行,报与SP模拟器差不多的错,即无法在一个Virtual Machine中运行。SFC的模拟器倒是可以运行,只是声音似乎有点问题。

posted @ | Feedb