locked
Customizing Task Form RRS feed

  • Question

  • Hi all.

    The task is to give user, information regarding total tasks that are due on any particular date (when user select any due date and want to see total tasks for this date).

    How can I provide this information over the Task form. I mean how to extend Task CRM form to provide this customization?

     


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    Thursday, October 28, 2010 10:20 AM

Answers

  • Hi Ali,

    Add one custom attribute on Task form say new_tasksdueonday. Make it readonly by dobul click that attribute and tick the field read only checkbox. Past the below code on onchage event of due date. save a form. publish it. You are done. Enjoy!!

    if(crmForm.all.scheduledend != null && crmForm.all.scheduledend.DataValue != null)
    {
    fetchXml = Fetch("<fetch mapping='logical' aggregate='true' version='1.0'><entity name='task'><attribute name='scheduledend' aggregate='countcolumn' alias='count' /><filter><condition attribute='scheduledend' operator='eq' value='" + FormatUtcDate(new Date(crmForm.all.scheduledend.DataValue)) + "' /></filter></entity></fetch>");
    }

    if(fetchXml != null)
    {
            var count= fetchXml.selectSingleNode("//count");
            if(count != null) crmForm.all.new_tasksdueonday.DataValue = count.text;

    }


    function Fetch( xmlRequestString )
    {

    var Xml = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
     Xml += GenerateAuthenticationHeader()
     Xml += "<soap:Body>";
     Xml += "<Fetch xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">";
     Xml += "<fetchXml>";
     Xml += FetchEncode(xmlRequestString); // HtmlEncode function
     Xml += "</fetchXml>";
     Xml += "</Fetch>";
     Xml += "</soap:Body>";
     Xml += "</soap:Envelope>";

    try {

     var XmlHttp = CreateXmlHttpObject(); // CreateXmlHttp function
     XmlHttp.open("POST", "/mscrmservices/2007/crmservice.asmx", false ); //Sync Request
     XmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
     XmlHttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
     XmlHttp.send(Xml);
    }
    catch(e)
    {
    debugger;
    }

     var XmlDoc = new ActiveXObject("Msxml2.DOMDocument");
     XmlDoc.async = false;
     XmlDoc.resolveExternals = false;
     XmlDoc.loadXML(XmlHttp.responseXML.text);

     return XmlDoc;
    }


    // CreateXmlHttp function
    function CreateXmlHttpObject()
    {
     var oXmlHttp = null;
     if (window.XMLHttpRequest) {
     oXmlHttp = new XMLHttpRequest();
     }
     else {
     var arrProgIds = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"];
     for (var iCount = 0; iCount < arrProgIds.length; iCount++) {
     try {
     oXmlHttp = new ActiveXObject(arrProgIds[iCount]);
     break;
     }
     catch (e) { }
     }
     }

     return oXmlHttp;
    }

    //HtmlEncode
    function FetchEncode( strInput )
    {
     var c;
     var HtmlEncode = '';

     if(strInput == null)
     {
     return null;
     }
     if (strInput == '')
     {
     return '';
     }

     for(var cnt = 0; cnt < strInput.length; cnt++)
     {
     c = strInput.charCodeAt(cnt);

     if (( ( c > 96 ) && ( c < 123 ) ) ||
     ( ( c > 64 ) && ( c < 91 ) ) ||
     ( c == 32 ) ||
     ( ( c > 47 ) && ( c < 58 ) ) ||
     ( c == 46 ) ||
     ( c == 44 ) ||
     ( c == 45 ) ||
     ( c == 95 ))
     {
     HtmlEncode = HtmlEncode + String.fromCharCode(c);
     }
     else
     {
     HtmlEncode = HtmlEncode + '&#' + c + ';';
     }
     }
     
     return HtmlEncode;
    }


    Thanks and Regards, Ankit Shah Business Development Head, Inkey Solutions, India Microsoft Certified Business Management Solutions Professionals http://www.inkeysolutions.com
    • Proposed as answer by Ankit_Shah Friday, October 29, 2010 11:13 AM
    • Marked as answer by Khadim Ali Friday, October 29, 2010 1:31 PM
    Friday, October 29, 2010 11:13 AM
  • Hello Ankit Shah!

    I just figured out (by testing fetchXml in stunnware's fetchXml wizard) that aggregate's value should be count only and should not be countcolumn as indicated in your given code. Thanks God, now it is returning counts but in consideration of time also. Please suggest suitable modification in your code so that all tasks for given due date could be counted.

    Thank you so much Ankit.


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    • Marked as answer by Khadim Ali Friday, October 29, 2010 1:32 PM
    Friday, October 29, 2010 1:09 PM
  • Hi Ali,

    Please change your condition as below

    <condition attribute="scheduledend" operator="on" value="2010-10-25T00:00:00" />

    make the operator as on instead of eq

     


    Thanks and Regards, Ankit Shah Business Development Head, Inkey Solutions, India Microsoft Certified Business Management Solutions Professionals http://www.inkeysolutions.com
    • Marked as answer by Khadim Ali Friday, October 29, 2010 1:32 PM
    Friday, October 29, 2010 1:24 PM

All replies

  • Hi Ali,

    You can create an iframe on task form whenever user selects any particular due date from a calendar you need to fetch the data and set the grid of the custom page of your iframe with the tasks of the selected date. 

     

    Thursday, October 28, 2010 12:10 PM
  • You can create an Advance Find view and then display it in an IFrame on the Task form. Following blog post will explain you the steps required;

    http://mscrm4ever.blogspot.com/2009/03/display-fetch-in-iframe-part-2.html


    MS CRM MVP :: uMar Khan :: Microsoft CRM Consultant (Blog :: http://umarkhan.wordpress.com)
    Thursday, October 28, 2010 12:55 PM
    Moderator
  • Thanks Ankit Shah and Umar Khan for your expert opinions.

    I think I was not able to properly clear my requirement. Sorry for that. Actually, I want only count of tasks for any particular due date. Still I should use an IFrame or Advanced Find? Or there could be any way to just obtain the integer of task counts?


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    Thursday, October 28, 2010 6:40 PM
  • Hi Ali,

    You could have a custom attribute on Task entity and should set it value through javascript by calling fetxml and get the count of the due tasks on the change event of the due date. You could also make the custom attribute as readonly on a Task form.

     


    -- Thanks and Regards, Ankit Shah Business Development Head, Inkey Solutions, India Microsoft Certified Business Management Solutions Professionals http://www.inkeysolutions.com
    • Proposed as answer by Ankit_Shah Friday, October 29, 2010 6:54 AM
    Friday, October 29, 2010 6:37 AM
  • Hi Ankit Shah

    Any link for sample code?

    Thank you very much.


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    Friday, October 29, 2010 7:57 AM
  • Hi Ali,

    Visit below link for more information

    http://social.microsoft.com/Forums/en-US/crm/thread/e53c0491-ada4-4e40-972d-04481393c77a 

    If you would like to achieve this with fetchxml then below is the sample of your fetch xml. Here the value of scheduledend would be the due date vlaue.

    <fetch mapping="logical" count="50" aggregate="true" version="1.0">
     <entity name="task">
      <attribute name="scheduledend" aggregate="countcolumn" alias="count" />
      <filter>
       <condition attribute="scheduledend" operator="eq" value="2010-10-25T00:00:00" />
      </filter>
     </entity>
    </fetch>

     


    -- Thanks and Regards, Ankit Shah Business Development Head, Inkey Solutions, India Microsoft Certified Business Management Solutions Professionals http://www.inkeysolutions.com
    • Proposed as answer by Ankit_Shah Friday, October 29, 2010 8:10 AM
    Friday, October 29, 2010 8:00 AM
  • Hi Ali,

    Add one custom attribute on Task form say new_tasksdueonday. Make it readonly by dobul click that attribute and tick the field read only checkbox. Past the below code on onchage event of due date. save a form. publish it. You are done. Enjoy!!

    if(crmForm.all.scheduledend != null && crmForm.all.scheduledend.DataValue != null)
    {
    fetchXml = Fetch("<fetch mapping='logical' aggregate='true' version='1.0'><entity name='task'><attribute name='scheduledend' aggregate='countcolumn' alias='count' /><filter><condition attribute='scheduledend' operator='eq' value='" + FormatUtcDate(new Date(crmForm.all.scheduledend.DataValue)) + "' /></filter></entity></fetch>");
    }

    if(fetchXml != null)
    {
            var count= fetchXml.selectSingleNode("//count");
            if(count != null) crmForm.all.new_tasksdueonday.DataValue = count.text;

    }


    function Fetch( xmlRequestString )
    {

    var Xml = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
     Xml += GenerateAuthenticationHeader()
     Xml += "<soap:Body>";
     Xml += "<Fetch xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">";
     Xml += "<fetchXml>";
     Xml += FetchEncode(xmlRequestString); // HtmlEncode function
     Xml += "</fetchXml>";
     Xml += "</Fetch>";
     Xml += "</soap:Body>";
     Xml += "</soap:Envelope>";

    try {

     var XmlHttp = CreateXmlHttpObject(); // CreateXmlHttp function
     XmlHttp.open("POST", "/mscrmservices/2007/crmservice.asmx", false ); //Sync Request
     XmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
     XmlHttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
     XmlHttp.send(Xml);
    }
    catch(e)
    {
    debugger;
    }

     var XmlDoc = new ActiveXObject("Msxml2.DOMDocument");
     XmlDoc.async = false;
     XmlDoc.resolveExternals = false;
     XmlDoc.loadXML(XmlHttp.responseXML.text);

     return XmlDoc;
    }


    // CreateXmlHttp function
    function CreateXmlHttpObject()
    {
     var oXmlHttp = null;
     if (window.XMLHttpRequest) {
     oXmlHttp = new XMLHttpRequest();
     }
     else {
     var arrProgIds = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"];
     for (var iCount = 0; iCount < arrProgIds.length; iCount++) {
     try {
     oXmlHttp = new ActiveXObject(arrProgIds[iCount]);
     break;
     }
     catch (e) { }
     }
     }

     return oXmlHttp;
    }

    //HtmlEncode
    function FetchEncode( strInput )
    {
     var c;
     var HtmlEncode = '';

     if(strInput == null)
     {
     return null;
     }
     if (strInput == '')
     {
     return '';
     }

     for(var cnt = 0; cnt < strInput.length; cnt++)
     {
     c = strInput.charCodeAt(cnt);

     if (( ( c > 96 ) && ( c < 123 ) ) ||
     ( ( c > 64 ) && ( c < 91 ) ) ||
     ( c == 32 ) ||
     ( ( c > 47 ) && ( c < 58 ) ) ||
     ( c == 46 ) ||
     ( c == 44 ) ||
     ( c == 45 ) ||
     ( c == 95 ))
     {
     HtmlEncode = HtmlEncode + String.fromCharCode(c);
     }
     else
     {
     HtmlEncode = HtmlEncode + '&#' + c + ';';
     }
     }
     
     return HtmlEncode;
    }


    -- Thanks and Regards, Ankit Shah Business Development Head, Inkey Solutions, India Microsoft Certified Business Management Solutions Professionals http://www.inkeysolutions.com
    • Proposed as answer by Ankit_Shah Friday, October 29, 2010 10:36 AM
    Friday, October 29, 2010 10:34 AM
  • Hi Ankit Shah!

    I have obtained javascript code for calling crmservice using that tool. But still having bit of difficulty in obtaining actual result.

    Look at this image that contains contents of resultXml.

    http://i432.photobucket.com/albums/qq45/alidotnet/CRM_ResultXML.png

    The actual value I want to obtain (TotalTasks=1) is highlighted in the image. But it seems due to some reason it is not returning result in true format. See &lt; and &gt; tags instead of < and > signs . How could I obtain actual result using resultXml.selectSingleNode("xxx").nodeTypedValue statement?

    Please suggest that last piece of code.

    Thank you so much for your time.


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    Friday, October 29, 2010 11:10 AM
  • Hi Ali,

    Add one custom attribute on Task form say new_tasksdueonday. Make it readonly by dobul click that attribute and tick the field read only checkbox. Past the below code on onchage event of due date. save a form. publish it. You are done. Enjoy!!

    if(crmForm.all.scheduledend != null && crmForm.all.scheduledend.DataValue != null)
    {
    fetchXml = Fetch("<fetch mapping='logical' aggregate='true' version='1.0'><entity name='task'><attribute name='scheduledend' aggregate='countcolumn' alias='count' /><filter><condition attribute='scheduledend' operator='eq' value='" + FormatUtcDate(new Date(crmForm.all.scheduledend.DataValue)) + "' /></filter></entity></fetch>");
    }

    if(fetchXml != null)
    {
            var count= fetchXml.selectSingleNode("//count");
            if(count != null) crmForm.all.new_tasksdueonday.DataValue = count.text;

    }


    function Fetch( xmlRequestString )
    {

    var Xml = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
     Xml += GenerateAuthenticationHeader()
     Xml += "<soap:Body>";
     Xml += "<Fetch xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">";
     Xml += "<fetchXml>";
     Xml += FetchEncode(xmlRequestString); // HtmlEncode function
     Xml += "</fetchXml>";
     Xml += "</Fetch>";
     Xml += "</soap:Body>";
     Xml += "</soap:Envelope>";

    try {

     var XmlHttp = CreateXmlHttpObject(); // CreateXmlHttp function
     XmlHttp.open("POST", "/mscrmservices/2007/crmservice.asmx", false ); //Sync Request
     XmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
     XmlHttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
     XmlHttp.send(Xml);
    }
    catch(e)
    {
    debugger;
    }

     var XmlDoc = new ActiveXObject("Msxml2.DOMDocument");
     XmlDoc.async = false;
     XmlDoc.resolveExternals = false;
     XmlDoc.loadXML(XmlHttp.responseXML.text);

     return XmlDoc;
    }


    // CreateXmlHttp function
    function CreateXmlHttpObject()
    {
     var oXmlHttp = null;
     if (window.XMLHttpRequest) {
     oXmlHttp = new XMLHttpRequest();
     }
     else {
     var arrProgIds = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"];
     for (var iCount = 0; iCount < arrProgIds.length; iCount++) {
     try {
     oXmlHttp = new ActiveXObject(arrProgIds[iCount]);
     break;
     }
     catch (e) { }
     }
     }

     return oXmlHttp;
    }

    //HtmlEncode
    function FetchEncode( strInput )
    {
     var c;
     var HtmlEncode = '';

     if(strInput == null)
     {
     return null;
     }
     if (strInput == '')
     {
     return '';
     }

     for(var cnt = 0; cnt < strInput.length; cnt++)
     {
     c = strInput.charCodeAt(cnt);

     if (( ( c > 96 ) && ( c < 123 ) ) ||
     ( ( c > 64 ) && ( c < 91 ) ) ||
     ( c == 32 ) ||
     ( ( c > 47 ) && ( c < 58 ) ) ||
     ( c == 46 ) ||
     ( c == 44 ) ||
     ( c == 45 ) ||
     ( c == 95 ))
     {
     HtmlEncode = HtmlEncode + String.fromCharCode(c);
     }
     else
     {
     HtmlEncode = HtmlEncode + '&#' + c + ';';
     }
     }
     
     return HtmlEncode;
    }


    Thanks and Regards, Ankit Shah Business Development Head, Inkey Solutions, India Microsoft Certified Business Management Solutions Professionals http://www.inkeysolutions.com
    • Proposed as answer by Ankit_Shah Friday, October 29, 2010 11:13 AM
    • Marked as answer by Khadim Ali Friday, October 29, 2010 1:31 PM
    Friday, October 29, 2010 11:13 AM
  • Hello Ankit_Shat

    I tried with your code (copy/paste) but still not able to get the desired result. Done everything as you instructed.

    Set Due to 7/8/2010

    Time to 9:30 AM

    Then returned these variables in alert and they are showing as follows.

    FormatUtcDate(new Date(crmForm.all.scheduledend.DataValue)) = 2010-07-08T09:30:00

    fetchXml = empty string (not null)

    fetchXml.xml = empty string (but not null)

    count = null

    I have 1 task on that date and time in the system.

    Also I dont want to consider time while counting tasks for any particular date.


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    Friday, October 29, 2010 12:46 PM
  • In addition to the issue described in previous post I am posting the code for Due Date's onChange() event here:

    if(crmForm.all.scheduledend != null && crmForm.all.scheduledend.DataValue != null)
    {
    fetchXml = Fetch("<fetch mapping='logical' aggregate='true' version='1.0'><entity name='task'><attribute name='scheduledend' aggregate='countcolumn' alias='count' /><filter><condition attribute='scheduledend' operator='eq' value='" + FormatUtcDate(new Date(crmForm.all.scheduledend.DataValue)) + "' /></filter></entity></fetch>");
    }
    
    if(fetchXml != null)
    {
        alert("Formatted Date: " + FormatUtcDate(new Date(crmForm.all.scheduledend.DataValue)))
        alert("fetchXml: " + fetchXml);
        alert("fetchXml.xml: " + fetchXml.xml);
    
        var count= fetchXml.selectSingleNode("//count");
    
        alert("count: " + count);
    
        if(count != null) crmForm.all.new_tasksdueonday.DataValue = count.text;
    
    }
    
    
    function Fetch( xmlRequestString )
    {
    
    var Xml = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
     Xml += GenerateAuthenticationHeader()
     Xml += "<soap:Body>";
     Xml += "<Fetch xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">";
     Xml += "<fetchXml>";
     Xml += FetchEncode(xmlRequestString); // HtmlEncode function
     Xml += "</fetchXml>";
     Xml += "</Fetch>";
     Xml += "</soap:Body>";
     Xml += "</soap:Envelope>";
    
    try {
    
     var XmlHttp = CreateXmlHttpObject(); // CreateXmlHttp function
     XmlHttp.open("POST", "/mscrmservices/2007/crmservice.asmx", false ); //Sync Request
     XmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
     XmlHttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
     XmlHttp.send(Xml);
    }
    catch(e)
    {
    debugger;
    }
    
     var XmlDoc = new ActiveXObject("Msxml2.DOMDocument");
     XmlDoc.async = false;
     XmlDoc.resolveExternals = false;
     XmlDoc.loadXML(XmlHttp.responseXML.text);
    
     return XmlDoc;
    }
    
    
    // CreateXmlHttp function
    function CreateXmlHttpObject()
    {
     var oXmlHttp = null;
     if (window.XMLHttpRequest) {
     oXmlHttp = new XMLHttpRequest();
     }
     else {
     var arrProgIds = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"];
     for (var iCount = 0; iCount < arrProgIds.length; iCount++) {
     try {
     oXmlHttp = new ActiveXObject(arrProgIds[iCount]);
     break;
     }
     catch (e) { }
     }
     }
    
     return oXmlHttp;
    }
    
    //HtmlEncode
    function FetchEncode( strInput )
    {
     var c;
     var HtmlEncode = '';
    
     if(strInput == null)
     {
     return null;
     }
     if (strInput == '')
     {
     return '';
     }
    
     for(var cnt = 0; cnt < strInput.length; cnt++)
     {
     c = strInput.charCodeAt(cnt);
    
     if (( ( c > 96 ) && ( c < 123 ) ) ||
     ( ( c > 64 ) && ( c < 91 ) ) ||
     ( c == 32 ) ||
     ( ( c > 47 ) && ( c < 58 ) ) ||
     ( c == 46 ) ||
     ( c == 44 ) ||
     ( c == 45 ) ||
     ( c == 95 ))
     {
     HtmlEncode = HtmlEncode + String.fromCharCode(c);
     }
     else
     {
     HtmlEncode = HtmlEncode + '&#' + c + ';';
     }
     }
     
     return HtmlEncode;
    }
    


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    Friday, October 29, 2010 12:57 PM
  • Hello Ankit Shah!

    I just figured out (by testing fetchXml in stunnware's fetchXml wizard) that aggregate's value should be count only and should not be countcolumn as indicated in your given code. Thanks God, now it is returning counts but in consideration of time also. Please suggest suitable modification in your code so that all tasks for given due date could be counted.

    Thank you so much Ankit.


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    • Marked as answer by Khadim Ali Friday, October 29, 2010 1:32 PM
    Friday, October 29, 2010 1:09 PM
  • Hi Ali,

    Please change your condition as below

    <condition attribute="scheduledend" operator="on" value="2010-10-25T00:00:00" />

    make the operator as on instead of eq

     


    Thanks and Regards, Ankit Shah Business Development Head, Inkey Solutions, India Microsoft Certified Business Management Solutions Professionals http://www.inkeysolutions.com
    • Marked as answer by Khadim Ali Friday, October 29, 2010 1:32 PM
    Friday, October 29, 2010 1:24 PM
  • I did not get you here

    make sure you have changed your fetch xml as below

    fetchXml = Fetch("<fetch mapping='logical' aggregate='true' version='1.0'><entity name='task'><attribute name='scheduledend' aggregate='countcolumn' alias='count' /><filter><condition attribute='scheduledend' operator='on' value='" + FormatUtcDate(new Date(crmForm.all.scheduledend.DataValue)) + "' /></filter></entity></fetch>");


    Thanks and Regards, Ankit Shah Business Development Head, Inkey Solutions, India Microsoft Certified Business Management Solutions Professionals http://www.inkeysolutions.com
    Friday, October 29, 2010 1:30 PM
  • That's done!

    Grrrrrrrrrr8 Ankit. You are champion sir.

    Thank you so much.

    Ab ye code muje samjhaye ga kaun :P :)


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    Friday, October 29, 2010 1:31 PM
  • Make sure you have removed all "debugger" from the code.
    Thanks and Regards, Ankit Shah Business Development Head, Inkey Solutions, India Microsoft Certified Business Management Solutions Professionals http://www.inkeysolutions.com
    Friday, October 29, 2010 1:41 PM
  • ok.

    thanks once again.


    Paradise lies at the feet of thy mother. - Prophet Mohammed (PBUH)
    Friday, October 29, 2010 1:45 PM