/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Reflection;
using log4net;
using OpenMetaverse;
namespace OpenSim.Framework.Communications.Cache
{
internal delegate void AddItemDelegate(InventoryItemBase itemInfo);
internal delegate void UpdateItemDelegate(InventoryItemBase itemInfo);
internal delegate void DeleteItemDelegate(UUID itemID);
internal delegate void QueryItemDelegate(UUID itemID);
internal delegate void QueryFolderDelegate(UUID folderID);
internal delegate void CreateFolderDelegate(string folderName, UUID folderID, ushort folderType, UUID parentID);
internal delegate void MoveFolderDelegate(UUID folderID, UUID parentID);
internal delegate void PurgeFolderDelegate(UUID folderID);
internal delegate void UpdateFolderDelegate(string name, UUID folderID, ushort type, UUID parentID);
internal delegate void SendInventoryDescendentsDelegate(
IClientAPI client, UUID folderID, bool fetchFolders, bool fetchItems);
public delegate void OnItemReceivedDelegate(UUID itemID);
///
/// Stores user profile and inventory data received from backend services for a particular user.
///
public class CachedUserInfo
{
public event OnItemReceivedDelegate OnItemReceived;
private static readonly ILog m_log
= LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
///
/// The comms manager holds references to services (user, grid, inventory, etc.)
///
private readonly CommunicationsManager m_commsManager;
public UserProfileData UserProfile { get { return m_userProfile; } }
private UserProfileData m_userProfile;
///
/// Have we received the user's inventory from the inventory service?
///
public bool HasReceivedInventory { get { return m_hasReceivedInventory; } }
private bool m_hasReceivedInventory;
///
/// Inventory requests waiting for receipt of this user's inventory from the inventory service.
///
private readonly IList m_pendingRequests = new List();
///
/// The root folder of this user's inventory. Returns null if the root folder has not yet been received.
///
public InventoryFolderImpl RootFolder { get { return m_rootFolder; } }
private InventoryFolderImpl m_rootFolder;
public UUID SessionID
{
get { return m_session_id; }
set { m_session_id = value; }
}
private UUID m_session_id = UUID.Zero;
///
/// Constructor
///
///
///
public CachedUserInfo(CommunicationsManager commsManager, UserProfileData userProfile)
{
m_commsManager = commsManager;
m_userProfile = userProfile;
}
///
/// This allows a request to be added to be processed once we receive a user's inventory
/// from the inventory service. If we already have the inventory, the request
/// is executed immediately instead.
///
///
protected void AddRequest(IInventoryRequest request)
{
lock (m_pendingRequests)
{
if (HasReceivedInventory)
{
request.Execute();
}
else
{
m_pendingRequests.Add(request);
}
}
}
///
/// Helper function for InventoryReceive() - Store a folder temporarily until we've received entire folder list
///
///
private void AddFolderToDictionary(InventoryFolderImpl folder, IDictionary> dictionary)
{
UUID parentFolderId = folder.ParentID;
if (dictionary.ContainsKey(parentFolderId))
dictionary[parentFolderId].Add(folder);
else
{
IList folders = new List();
folders.Add(folder);
dictionary[parentFolderId] = folders;
}
}
///
/// Recursively, in depth-first order, add all the folders we've received (stored
/// in a dictionary indexed by parent ID) into the tree that describes user folder
/// heirarchy
/// Any folder that is resolved into the tree is also added to resolvedFolderDictionary,
/// indexed by folder ID.
///
///
/// A
///
private void ResolveReceivedFolders(InventoryFolderImpl parentFolder,
IDictionary> receivedFolderDictionary,
IDictionary resolvedFolderDictionary)
{
if (receivedFolderDictionary.ContainsKey(parentFolder.ID))
{
List resolvedFolders = new List(); // Folders we've resolved with this invocation
foreach (InventoryFolderImpl folder in receivedFolderDictionary[parentFolder.ID])
{
if (parentFolder.ContainsChildFolder(folder.ID))
{
m_log.WarnFormat(
"[INVENTORY CACHE]: Received folder {0} {1} from inventory service which has already been received",
folder.Name, folder.ID);
}
else
{
if (resolvedFolderDictionary.ContainsKey(folder.ID))
{
m_log.WarnFormat(
"[INVENTORY CACHE]: Received folder {0} {1} from inventory service has already been received but with different parent",
folder.Name, folder.ID);
}
else
{
resolvedFolders.Add(folder);
resolvedFolderDictionary[folder.ID] = folder;
parentFolder.AddChildFolder(folder);
}
}
} // foreach (folder in pendingCategorizationFolders[parentFolder.ID])
receivedFolderDictionary.Remove(parentFolder.ID);
foreach (InventoryFolderImpl folder in resolvedFolders)
ResolveReceivedFolders(folder, receivedFolderDictionary, resolvedFolderDictionary);
} // if (receivedFolderDictionary.ContainsKey(parentFolder.ID))
}
///
/// Drop all cached inventory.
///
public void DropInventory()
{
m_log.Debug("[INVENTORY CACHE]: DropInventory called");
// Make sure there aren't pending requests around when we do this
// FIXME: There is still a race condition where an inventory operation can be requested (since these aren't being locked).
// Will have to extend locking to exclude this very soon.
lock (m_pendingRequests)
{
m_hasReceivedInventory = false;
m_rootFolder = null;
}
}
///
/// Fetch inventory for this user.
///
/// This has to be executed as a separate step once user information is retreived.
/// This will occur synchronously if the inventory service is in the same process as this class, and
/// asynchronously otherwise.
public void FetchInventory()
{
if (m_commsManager.SecureInventoryService != null)
{
m_commsManager.SecureInventoryService.RequestInventoryForUser(
UserProfile.ID, SessionID, InventoryReceive);
}
else
{
m_commsManager.InventoryService.RequestInventoryForUser(UserProfile.ID, InventoryReceive);
}
}
///
/// Callback invoked when the inventory is received from an async request to the inventory service
///
///
///
public void InventoryReceive(ICollection folders, ICollection items)
{
// FIXME: Exceptions thrown upwards never appear on the console. Could fix further up if these
// are simply being swallowed
try
{
// collection of all received folders, indexed by their parent ID
IDictionary> receivedFolders =
new Dictionary>();
// collection of all folders that have been placed into the folder heirarchy starting at m_rootFolder
// This dictonary exists so we don't have to do an InventoryFolderImpl.FindFolder(), which is O(n) on the
// number of folders in our inventory.
// Maybe we should make this structure a member so we can skip InventoryFolderImpl.FindFolder() calls later too?
IDictionary resolvedFolders =
new Dictionary();
// Take all received folders, find the root folder, and put ther rest into
// the pendingCategorizationFolders collection
foreach (InventoryFolderImpl folder in folders)
AddFolderToDictionary(folder, receivedFolders);
if (!receivedFolders.ContainsKey(UUID.Zero))
throw new Exception("Database did not return a root inventory folder");
else
{
IList rootFolderList = receivedFolders[UUID.Zero];
m_rootFolder = rootFolderList[0];
resolvedFolders[m_rootFolder.ID] = m_rootFolder;
if (rootFolderList.Count > 1)
{
for (int i = 1; i < rootFolderList.Count; i++)
{
m_log.WarnFormat(
"[INVENTORY CACHE]: Discarding extra root folder {0}. Using previously received root folder {1}",
rootFolderList[i].ID, RootFolder.ID);
}
}
receivedFolders.Remove(UUID.Zero);
}
// Now take the pendingCategorizationFolders collection, and turn that into a tree,
// with the root being RootFolder
if (RootFolder != null)
ResolveReceivedFolders(RootFolder, receivedFolders, resolvedFolders);
// Generate a warning for folders that are not part of the heirarchy
foreach (KeyValuePair> folderList in receivedFolders)
{
foreach (InventoryFolderImpl folder in folderList.Value)
m_log.WarnFormat("[INVENTORY CACHE]: Malformed Database: Unresolved Pending Folder {0}", folder.Name);
}
// Take all ther received items and put them into the folder tree heirarchy
foreach (InventoryItemBase item in items) {
InventoryFolderImpl folder = resolvedFolders.ContainsKey(item.Folder) ? resolvedFolders[item.Folder] : null;
ItemReceive(item, folder);
}
}
catch (Exception e)
{
m_log.ErrorFormat("[INVENTORY CACHE]: Error processing inventory received from inventory service, {0}", e);
}
// Deal with pending requests
lock (m_pendingRequests)
{
// We're going to change inventory status within the lock to avoid a race condition
// where requests are processed after the AddRequest() method has been called.
m_hasReceivedInventory = true;
foreach (IInventoryRequest request in m_pendingRequests)
{
request.Execute();
}
}
}
///
/// Callback invoked when an item is received from an async request to the inventory service.
///
/// We're assuming here that items are always received after all the folders
/// received.
/// If folder is null, we will search for it starting from RootFolder (an O(n) operation),
/// otherwise we'll just put it into folder
///
///
private void ItemReceive(InventoryItemBase itemInfo, InventoryFolderImpl folder)
{
// m_log.DebugFormat(
// "[INVENTORY CACHE]: Received item {0} {1} for user {2}",
// itemInfo.Name, itemInfo.ID, userID);
if (folder == null && RootFolder != null)
folder = RootFolder.FindFolder(itemInfo.Folder);
if (null == folder)
{
m_log.WarnFormat(
"Received item {0} {1} but its folder {2} does not exist",
itemInfo.Name, itemInfo.ID, itemInfo.Folder);
return;
}
lock (folder.Items)
{
folder.Items[itemInfo.ID] = itemInfo;
}
if (OnItemReceived != null)
OnItemReceived(itemInfo.ID);
}
///
/// Create a folder in this agent's inventory.
///
///
/// If the inventory service has not yet delievered the inventory
/// for this user then the request will be queued.
///
///
///
public bool CreateFolder(string folderName, UUID folderID, ushort folderType, UUID parentID)
{
// m_log.DebugFormat(
// "[AGENT INVENTORY]: Creating inventory folder {0} {1} for {2} {3}", folderID, folderName, remoteClient.Name, remoteClient.AgentId);
if (m_hasReceivedInventory)
{
InventoryFolderImpl parentFolder = RootFolder.FindFolder(parentID);
if (null == parentFolder)
{
m_log.WarnFormat(
"[AGENT INVENTORY]: Tried to create folder {0} {1} but the parent {2} does not exist",
folderName, folderID, parentID);
return false;
}
InventoryFolderImpl createdFolder = parentFolder.CreateChildFolder(folderID, folderName, folderType);
if (createdFolder != null)
{
InventoryFolderBase createdBaseFolder = new InventoryFolderBase();
createdBaseFolder.Owner = createdFolder.Owner;
createdBaseFolder.ID = createdFolder.ID;
createdBaseFolder.Name = createdFolder.Name;
createdBaseFolder.ParentID = createdFolder.ParentID;
createdBaseFolder.Type = createdFolder.Type;
createdBaseFolder.Version = createdFolder.Version;
if (m_commsManager.SecureInventoryService != null)
{
m_commsManager.SecureInventoryService.AddFolder(createdBaseFolder, m_session_id);
}
else
{
m_commsManager.InventoryService.AddFolder(createdBaseFolder);
}
return true;
}
else
{
m_log.WarnFormat(
"[AGENT INVENTORY]: Tried to create folder {0} {1} but the folder already exists",
folderName, folderID);
return false;
}
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(CreateFolderDelegate), this, "CreateFolder"),
new object[] { folderName, folderID, folderType, parentID }));
return true;
}
}
///
/// Handle a client request to update the inventory folder
///
///
/// If the inventory service has not yet delievered the inventory
/// for this user then the request will be queued.
///
/// FIXME: We call add new inventory folder because in the data layer, we happen to use an SQL REPLACE
/// so this will work to rename an existing folder. Needless to say, to rely on this is very confusing,
/// and needs to be changed.
///
///
///
///
///
public bool UpdateFolder(string name, UUID folderID, ushort type, UUID parentID)
{
// m_log.DebugFormat(
// "[AGENT INVENTORY]: Updating inventory folder {0} {1} for {2} {3}", folderID, name, remoteClient.Name, remoteClient.AgentId);
if (m_hasReceivedInventory)
{
InventoryFolderImpl folder = RootFolder.FindFolder(folderID);
// Delegate movement if updated parent id isn't the same as the existing parentId
if (folder.ParentID != parentID)
MoveFolder(folderID, parentID);
InventoryFolderBase baseFolder = new InventoryFolderBase();
baseFolder.Owner = m_userProfile.ID;
baseFolder.ID = folderID;
baseFolder.Name = name;
baseFolder.ParentID = parentID;
baseFolder.Type = (short)type;
baseFolder.Version = RootFolder.Version;
if (m_commsManager.SecureInventoryService != null)
{
m_commsManager.SecureInventoryService.UpdateFolder(baseFolder, m_session_id);
}
else
{
m_commsManager.InventoryService.UpdateFolder(baseFolder);
}
folder.Name = name;
folder.Type = (short)type;
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(UpdateFolderDelegate), this, "UpdateFolder"),
new object[] { name, folderID, type, parentID }));
}
return true;
}
///
/// Handle an inventory folder move request from the client.
///
/// If the inventory service has not yet delievered the inventory
/// for this user then the request will be queued.
///
///
///
///
///
/// true if the delete was successful, or if it was queued pending folder receipt
/// false if the folder to be deleted did not exist.
///
public bool MoveFolder(UUID folderID, UUID parentID)
{
// m_log.DebugFormat(
// "[AGENT INVENTORY]: Moving inventory folder {0} into folder {1} for {2} {3}",
// parentID, remoteClient.Name, remoteClient.Name, remoteClient.AgentId);
if (m_hasReceivedInventory)
{
InventoryFolderBase baseFolder = new InventoryFolderBase();
baseFolder.Owner = m_userProfile.ID;
baseFolder.ID = folderID;
baseFolder.ParentID = parentID;
if (m_commsManager.SecureInventoryService != null)
{
m_commsManager.SecureInventoryService.MoveFolder(baseFolder, m_session_id);
}
else
{
m_commsManager.InventoryService.MoveFolder(baseFolder);
}
InventoryFolderImpl folder = RootFolder.FindFolder(folderID);
InventoryFolderImpl parentFolder = RootFolder.FindFolder(parentID);
if (parentFolder != null && folder != null)
{
InventoryFolderImpl oldParentFolder = RootFolder.FindFolder(folder.ParentID);
if (oldParentFolder != null)
{
oldParentFolder.RemoveChildFolder(folderID);
parentFolder.AddChildFolder(folder);
}
else
{
return false;
}
}
else
{
return false;
}
return true;
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(MoveFolderDelegate), this, "MoveFolder"),
new object[] { folderID, parentID }));
return true;
}
}
///
/// This method will delete all the items and folders in the given folder.
///
/// If the inventory service has not yet delievered the inventory
/// for this user then the request will be queued.
///
///
public bool PurgeFolder(UUID folderID)
{
// m_log.InfoFormat("[AGENT INVENTORY]: Purging folder {0} for {1} uuid {2}",
// folderID, remoteClient.Name, remoteClient.AgentId);
if (m_hasReceivedInventory)
{
InventoryFolderImpl purgedFolder = RootFolder.FindFolder(folderID);
if (purgedFolder != null)
{
// XXX Nasty - have to create a new object to hold details we already have
InventoryFolderBase purgedBaseFolder = new InventoryFolderBase();
purgedBaseFolder.Owner = purgedFolder.Owner;
purgedBaseFolder.ID = purgedFolder.ID;
purgedBaseFolder.Name = purgedFolder.Name;
purgedBaseFolder.ParentID = purgedFolder.ParentID;
purgedBaseFolder.Type = purgedFolder.Type;
purgedBaseFolder.Version = purgedFolder.Version;
if (m_commsManager.SecureInventoryService != null)
{
m_commsManager.SecureInventoryService.PurgeFolder(purgedBaseFolder, m_session_id);
}
else
{
m_commsManager.InventoryService.PurgeFolder(purgedBaseFolder);
}
purgedFolder.Purge();
return true;
}
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(PurgeFolderDelegate), this, "PurgeFolder"),
new object[] { folderID }));
return true;
}
return false;
}
///
/// Add an item to the user's inventory.
///
/// If the item has no folder set (i.e. it is UUID.Zero), then it is placed in the most appropriate folder
/// for that type.
///
public void AddItem(InventoryItemBase item)
{
if (m_hasReceivedInventory)
{
if (item.Folder == UUID.Zero)
{
InventoryFolderImpl f = FindFolderForType(item.AssetType);
if (f != null)
item.Folder = f.ID;
else
item.Folder = RootFolder.ID;
}
ItemReceive(item, null);
if (m_commsManager.SecureInventoryService != null)
{
m_commsManager.SecureInventoryService.AddItem(item, m_session_id);
}
else
{
m_commsManager.InventoryService.AddItem(item);
}
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(AddItemDelegate), this, "AddItem"),
new object[] { item }));
}
}
///
/// Update an item in the user's inventory
///
///
///
public void UpdateItem(InventoryItemBase item)
{
if (m_hasReceivedInventory)
{
if (m_commsManager.SecureInventoryService != null)
{
m_commsManager.SecureInventoryService.UpdateItem(item, m_session_id);
}
else
{
m_commsManager.InventoryService.UpdateItem(item);
}
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(UpdateItemDelegate), this, "UpdateItem"),
new object[] { item }));
}
}
///
/// Delete an item from the user's inventory
///
/// If the inventory service has not yet delievered the inventory
/// for this user then the request will be queued.
///
///
///
/// true on a successful delete or a if the request is queued.
/// Returns false on an immediate failure
///
public bool DeleteItem(UUID itemID)
{
if (m_hasReceivedInventory)
{
// XXX For historical reasons (grid comms), we need to retrieve the whole item in order to delete, even though
// really only the item id is required.
InventoryItemBase item = RootFolder.FindItem(itemID);
if (null == item)
{
m_log.WarnFormat("[AGENT INVENTORY]: Tried to delete item {0} which does not exist", itemID);
return false;
}
if (RootFolder.DeleteItem(item.ID))
{
if (m_commsManager.SecureInventoryService != null)
{
return m_commsManager.SecureInventoryService.DeleteItem(item, m_session_id);
}
else
{
return m_commsManager.InventoryService.DeleteItem(item);
}
}
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(DeleteItemDelegate), this, "DeleteItem"),
new object[] { itemID }));
return true;
}
return false;
}
///
/// Send details of the inventory items and/or folders in a given folder to the client.
///
///
///
///
///
/// true if the request was queued or successfully processed, false otherwise
public bool SendInventoryDecendents(IClientAPI client, UUID folderID, bool fetchFolders, bool fetchItems)
{
if (m_hasReceivedInventory)
{
InventoryFolderImpl folder;
if ((folder = RootFolder.FindFolder(folderID)) != null)
{
// m_log.DebugFormat(
// "[AGENT INVENTORY]: Found folder {0} for client {1}",
// folderID, remoteClient.AgentId);
client.SendInventoryFolderDetails(
client.AgentId, folderID, folder.RequestListOfItems(),
folder.RequestListOfFolders(), fetchFolders, fetchItems);
return true;
}
else
{
m_log.WarnFormat(
"[AGENT INVENTORY]: Could not find folder {0} requested by user {1} {2}",
folderID, client.Name, client.AgentId);
return false;
}
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(SendInventoryDescendentsDelegate), this, "SendInventoryDecendents", false, false),
new object[] { client, folderID, fetchFolders, fetchItems }));
return true;
}
}
///
/// Find an appropriate folder for the given asset type
///
///
/// null if no appropriate folder exists
public InventoryFolderImpl FindFolderForType(int type)
{
if (RootFolder == null)
return null;
return RootFolder.FindFolderForType(type);
}
// Load additional items that other regions have put into the database
// The item will be added tot he local cache. Returns true if the item
// was found and can be sent to the client
//
public bool QueryItem(InventoryItemBase item)
{
if (m_hasReceivedInventory)
{
InventoryItemBase invItem = RootFolder.FindItem(item.ID);
if (invItem != null)
{
// Item is in local cache, just update client
//
return true;
}
InventoryItemBase itemInfo = null;
if (m_commsManager.SecureInventoryService != null)
{
itemInfo = m_commsManager.SecureInventoryService.QueryItem(item, m_session_id);
}
else
{
itemInfo = m_commsManager.InventoryService.QueryItem(item);
}
if (itemInfo != null)
{
InventoryFolderImpl folder = RootFolder.FindFolder(itemInfo.Folder);
ItemReceive(itemInfo, folder);
return true;
}
return false;
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(QueryItemDelegate), this, "QueryItem"),
new object[] { item.ID }));
return true;
}
}
public bool QueryFolder(InventoryFolderBase folder)
{
if (m_hasReceivedInventory)
{
InventoryFolderBase invFolder = RootFolder.FindFolder(folder.ID);
if (invFolder != null)
{
// Folder is in local cache, just update client
//
return true;
}
InventoryFolderBase folderInfo = null;
if (m_commsManager.SecureInventoryService != null)
{
folderInfo = m_commsManager.SecureInventoryService.QueryFolder(folder, m_session_id);
}
else
{
folderInfo = m_commsManager.InventoryService.QueryFolder(folder);
}
if (folderInfo != null)
{
InventoryFolderImpl createdFolder = RootFolder.CreateChildFolder(folderInfo.ID, folderInfo.Name, (ushort)folderInfo.Type);
createdFolder.Version = folderInfo.Version;
createdFolder.Owner = folderInfo.Owner;
createdFolder.ParentID = folderInfo.ParentID;
return true;
}
return false;
}
else
{
AddRequest(
new InventoryRequest(
Delegate.CreateDelegate(typeof(QueryFolderDelegate), this, "QueryFolder"),
new object[] { folder.ID }));
return true;
}
}
}
///
/// Should be implemented by callers which require a callback when the user's inventory is received
///
public interface IInventoryRequest
{
///
/// This is the method executed once we have received the user's inventory by which the request can be fulfilled.
///
void Execute();
}
///
/// Generic inventory request
///
class InventoryRequest : IInventoryRequest
{
private Delegate m_delegate;
private Object[] m_args;
internal InventoryRequest(Delegate delegat, Object[] args)
{
m_delegate = delegat;
m_args = args;
}
public void Execute()
{
if (m_delegate != null)
m_delegate.DynamicInvoke(m_args);
}
}
}